├── .DS_Store ├── .commitlintrc.js ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.js ├── .prettierrc.js ├── .storybook ├── main.js ├── preview-head.html └── preview.js ├── CHANGELOG.md ├── LICENCE ├── README.md ├── demos ├── demo-block.tsx ├── demo-wrap.tsx └── index.css ├── package-lock.json ├── package.json ├── packages ├── .DS_Store ├── action-sheet │ ├── actionSheet.tsx │ ├── index.tsx │ ├── method.tsx │ └── styles │ │ └── index.scss ├── button │ ├── index.tsx │ └── styles │ │ └── index.scss ├── card │ ├── index.tsx │ └── styles │ │ └── index.scss ├── cell │ ├── cell-group.tsx │ ├── cell.tsx │ ├── index.tsx │ └── styles │ │ ├── cell-group.scss │ │ └── cell.scss ├── countdown │ ├── index.tsx │ ├── styles │ │ └── index.scss │ └── utils.ts ├── dialog │ ├── alert.tsx │ ├── confirm.tsx │ ├── dialog-action-button.tsx │ ├── dialog.tsx │ ├── index.tsx │ ├── show.tsx │ └── styles │ │ └── index.scss ├── divider │ ├── index.tsx │ └── styles │ │ └── index.scss ├── ellipsis │ ├── index.tsx │ ├── styles │ │ └── index.scss │ └── utils.ts ├── error-block │ ├── errorImage.tsx │ ├── index.tsx │ └── styles │ │ └── index.scss ├── grid │ ├── grid-item.tsx │ ├── grid.tsx │ ├── index.tsx │ └── styles │ │ ├── grid-item.scss │ │ └── grid.scss ├── hooks │ ├── index.tsx │ ├── useEffectOnce.tsx │ ├── useIntersectionObserver.tsx │ ├── useIsomorphicLayoutEffect.tsx │ ├── useLatest.tsx │ ├── useLockFn.tsx │ ├── useMemoizedFn.tsx │ ├── useMount.tsx │ ├── useReadLocalStorage.tsx │ ├── useResizeObserver.tsx │ ├── useScrollLock.tsx │ ├── useThrottleFn.tsx │ ├── useUnmount.tsx │ ├── useUpdateEffect.tsx │ └── useUpdateIsomorphicLayoutEffect.tsx ├── image │ └── index.tsx ├── index.tsx ├── infinite-scroll │ ├── index.tsx │ └── styles │ │ └── index.scss ├── input │ ├── index.tsx │ └── styles │ │ └── index.scss ├── mask │ ├── index.tsx │ └── styles │ │ └── index.scss ├── nav-bar │ ├── index.tsx │ └── styles │ │ └── index.scss ├── popup │ ├── index.tsx │ └── styles │ │ └── index.scss ├── pull-to-refresh │ ├── constants.ts │ ├── index.tsx │ ├── styles │ │ └── index.scss │ ├── types.ts │ └── utils.ts ├── search-bar │ ├── index.tsx │ └── styles │ │ └── index.scss ├── selector │ ├── CheckMark.tsx │ ├── index.tsx │ └── styles │ │ └── index.scss ├── sidebar │ ├── index.tsx │ ├── sidebar-item.tsx │ ├── sidebar.tsx │ └── styles │ │ └── index.scss ├── slider │ ├── index.tsx │ ├── slider.tsx │ ├── styles │ │ ├── slider.scss │ │ └── thumb.scss │ └── thumb.tsx ├── space │ ├── index.tsx │ └── styles │ │ └── index.scss ├── spinner-loading │ ├── index.tsx │ └── styles │ │ └── index.scss ├── styles │ ├── base.scss │ ├── index.scss │ ├── reset.scss │ └── variable.scss ├── swiper │ ├── index.tsx │ ├── styles │ │ ├── swiper-item.scss │ │ ├── swiper-page-indicator.scss │ │ └── swiper.scss │ ├── swiper-item.tsx │ ├── swiper-page-indicator.tsx │ ├── swiper.tsx │ └── utils.ts ├── tabs │ ├── index.tsx │ ├── styles │ │ └── index.scss │ ├── tab.tsx │ └── tabs.tsx ├── toast │ ├── index.tsx │ ├── methods.tsx │ ├── styles │ │ └── index.scss │ └── toast.tsx └── utils │ ├── event.ts │ ├── render-imperatively.tsx │ ├── render.tsx │ ├── scroll.ts │ ├── traverse-react-node.tsx │ ├── utils.ts │ └── validate.ts ├── scripts ├── build.js └── utils │ ├── babel.js │ ├── getBabelConfig.js │ ├── log.js │ └── randomColor.js ├── stories ├── action-sheet │ ├── index.scss │ └── index.stories.tsx ├── button │ └── index.stories.tsx ├── card │ └── index.stories.tsx ├── cell │ └── index.stories.tsx ├── countdown │ ├── index.scss │ └── index.stories.tsx ├── dialog │ └── index.stories.tsx ├── divider │ └── index.stories.tsx ├── ellipsis │ └── index.stories.tsx ├── error-block │ ├── index.scss │ └── index.stories.tsx ├── grid │ ├── index.scss │ └── index.stories.tsx ├── image │ ├── img.png │ └── index.stories.tsx ├── infinite-scroll │ └── index.stories.tsx ├── input │ └── index.stories.tsx ├── mask │ └── index.stories.tsx ├── nav-bar │ └── index.stories.tsx ├── popup │ ├── index.scss │ └── index.stories.tsx ├── pull-to-refresh │ └── index.stories.tsx ├── search-bar │ └── index.stories.tsx ├── selector │ └── index.stories.tsx ├── sidebar │ └── index.stories.tsx ├── slider │ └── index.stories.tsx ├── space │ ├── index.scss │ └── index.stories.tsx ├── spinner-loading │ └── index.stories.tsx ├── swiper │ ├── index.scss │ └── index.stories.tsx ├── tabs │ ├── index.scss │ └── index.stories.tsx └── toast │ └── index.stories.tsx ├── tsconfig.json └── typings.d.ts /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoyage/react-mobile-ui/e0d17002078e43a0fdfc75407f78b3e937e4127d/.DS_Store -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@taoyage/configs/commitlint'); 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | es 2 | lib 3 | public 4 | .commitlintrc.js 5 | .lintstagedrc.js 6 | .prettierrc.js 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const configs = require('@taoyage/configs/eslint-ts'); 2 | 3 | module.exports = { 4 | ...configs, 5 | settings: { 6 | ...configs.settings, 7 | 'import/resolver': { 8 | alias: { 9 | map: [['@', './packages']], 10 | extensions: ['.ts', '.tsx', '.js', '.json'], 11 | }, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | esm 3 | cjs 4 | lib 5 | es 6 | .idea/ 7 | storybook-static 8 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@taoyage/configs/lintstaged'); 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@taoyage/configs/prettier'); 2 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); 4 | 5 | function resolve(...dirs) { 6 | return path.join(__dirname, '../', ...dirs); 7 | } 8 | 9 | module.exports = { 10 | stories: ['../stories/**/*.stories.mdx', '../stories/**/*.stories.@(js|jsx|ts|tsx)'], 11 | addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], 12 | framework: '@storybook/react', 13 | core: { 14 | builder: '@storybook/builder-webpack5', 15 | }, 16 | webpackFinal: (config, { configType }) => { 17 | const isProd = configType === 'PRODUCTION'; 18 | const env = isProd ? 'production' : 'development'; 19 | 20 | config.mode = env; 21 | 22 | config.devtool = isProd ? false : 'cheap-module-source-map'; 23 | 24 | config.resolve = { 25 | ...config.resolve, 26 | alias: { 27 | ...config.resolve.alias, 28 | '@': resolve('packages'), 29 | }, 30 | }; 31 | 32 | config.module.rules.push({ 33 | test: /\.scss$/, 34 | use: [ 35 | MiniCssExtractPlugin.loader, 36 | 'css-loader', 37 | { 38 | loader: 'sass-loader', 39 | }, 40 | ], 41 | include: [resolve('stories'), resolve('packages'), resolve('demos')], 42 | }); 43 | 44 | config.plugins.push( 45 | new ReactRefreshWebpackPlugin({ 46 | overlay: false, 47 | }) 48 | ); 49 | 50 | config.plugins.push( 51 | new MiniCssExtractPlugin({ 52 | filename: `[name]${isProd ? '.[hash]' : ''}.css`, 53 | chunkFilename: `[id]${isProd ? '.[hash]' : ''}.css`, 54 | }) 55 | ); 56 | 57 | return config; 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import '../packages/styles/index.scss'; 2 | 3 | export const parameters = { 4 | actions: { argTypesRegex: '^on[A-Z].*' }, 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/, 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 taoyage 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 |

react-mobile-ui

2 | 3 |

📱 A mobile component library based on the React framework 4 | 5 |

6 | document 7 |

8 | 9 | ## ✨ Feature 10 | 11 | - 💎 A set of high-quality React components out of the box. 12 | - 💪 Written in TypeScript, providing a complete type definition. 13 | - 📝 Provide complete documentation. 14 | - 😎 Support on-demand import and Tree Shaking. 15 | - ⚡️ Support Vite and Webpack. 16 | - 🌵 Modern browsers. 17 | - 🌝 Support SSR. 18 | 19 | ### Installation 20 | 21 | ```javascript 22 | $ npm install @taoyage/react-mobile-ui --save 23 | or 24 | $ pnpm install @taoyage/react-mobile-ui 25 | or 26 | $ yarn install @taoyage/react-mobile-ui 27 | ``` 28 | 29 | #### Code Snippet 30 | 31 | ```jsx 32 | import ReactDOM from 'react-dom/client'; 33 | import { Button } from '@taoyage/react-mobile-ui'; 34 | 35 | function App() { 36 | return ; 37 | } 38 | 39 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); 40 | ``` 41 | -------------------------------------------------------------------------------- /demos/demo-block.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './index.css'; 4 | 5 | interface DemoBlock { 6 | title?: React.ReactNode; 7 | children?: React.ReactNode; 8 | style?: React.CSSProperties; 9 | } 10 | 11 | const DemoBlock: React.FC = (props) => { 12 | return ( 13 | <> 14 |
{props.title}
15 |
16 | {props.children} 17 |
18 | 19 | ); 20 | }; 21 | 22 | export default DemoBlock; 23 | -------------------------------------------------------------------------------- /demos/demo-wrap.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './index.css'; 4 | 5 | interface DemoWrapProps { 6 | children?: React.ReactNode; 7 | } 8 | 9 | const DemoWrap: React.FC = (props) => { 10 | return
{props.children}
; 11 | }; 12 | 13 | export default DemoWrap; 14 | -------------------------------------------------------------------------------- /demos/index.css: -------------------------------------------------------------------------------- 1 | .demo-wrap { 2 | background-color: #eee; 3 | border-radius: 4px; 4 | width: 375px; 5 | border: 1px solid #dce1e5; 6 | height: 100%; 7 | overflow: hidden; 8 | overflow-y: auto; 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | 13 | .demo-block-header { 14 | font-size: 14px; 15 | padding: 10px; 16 | } 17 | 18 | .demo-block-content { 19 | padding: 10px; 20 | background-color: #fff; 21 | overflow: hidden; 22 | overflow-y: auto; 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@taoyage/react-mobile-ui", 3 | "version": "1.8.9", 4 | "description": "A react mobile components lib", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "typings": "lib/index.d.ts", 8 | "scripts": { 9 | "storybook": "start-storybook -p 6006", 10 | "build-storybook": "build-storybook", 11 | "storybook-docs": "start-storybook --docs --no-manager-cache", 12 | "build-storybook-docs": "build-storybook --docs --no-manager-cache", 13 | "docs:deploy": "gh-pages -d storybook-static", 14 | "deploy": "npm run build-storybook-docs && npm run docs:deploy", 15 | "build": "node ./scripts/build.js", 16 | "prepare": "husky install", 17 | "postpublish": "npm run changelog", 18 | "prepublishOnly": "npm run build", 19 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md" 20 | }, 21 | "keywords": [ 22 | "react", 23 | "mobile", 24 | "components", 25 | "ui" 26 | ], 27 | "files": [ 28 | "es", 29 | "lib" 30 | ], 31 | "engines": { 32 | "node": ">= 16.15.0" 33 | }, 34 | "sideEffects": [ 35 | "stories/**/*.scss", 36 | "packages/**/*.scss", 37 | "es/**/*.scss", 38 | "lib/**/*.scss" 39 | ], 40 | "repository": { 41 | "type": "git", 42 | "url": "https://github.com/taoyage/react-mobile-ui.git" 43 | }, 44 | "author": "taoyage", 45 | "license": "MIT", 46 | "devDependencies": { 47 | "@babel/core": "^7.18.9", 48 | "@babel/plugin-transform-runtime": "^7.18.9", 49 | "@babel/preset-env": "^7.18.9", 50 | "@babel/preset-react": "^7.17.12", 51 | "@babel/preset-typescript": "^7.17.12", 52 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", 53 | "@storybook/addon-actions": "^6.5.9", 54 | "@storybook/addon-essentials": "^6.5.9", 55 | "@storybook/addon-interactions": "^6.5.9", 56 | "@storybook/addon-links": "^6.5.9", 57 | "@storybook/builder-webpack5": "^6.5.9", 58 | "@storybook/manager-webpack5": "^6.5.9", 59 | "@storybook/react": "^6.5.9", 60 | "@storybook/testing-library": "^0.0.11", 61 | "@taoyage/configs": "^1.3.1", 62 | "@types/node": "^18.0.6", 63 | "@types/react": "^18.0.15", 64 | "@types/react-dom": "^18.0.6", 65 | "@types/react-is": "^17.0.3", 66 | "@types/react-transition-group": "^4.4.5", 67 | "babel-loader": "^8.2.5", 68 | "chalk": "^4.0.0", 69 | "conventional-changelog-cli": "^2.2.2", 70 | "gh-pages": "^4.0.0", 71 | "gulp-if": "^3.0.0", 72 | "gulp-image": "^5.1.0", 73 | "gulp-sass": "^5.1.0", 74 | "gulp-style-aliases": "^1.1.11", 75 | "gulp-ts-alias": "^1.3.0", 76 | "gulp-typescript": "^6.0.0-alpha.1", 77 | "html-webpack-plugin": "^5.5.0", 78 | "husky": "^8.0.0", 79 | "mini-css-extract-plugin": "^2.6.0", 80 | "sass": "^1.52.2", 81 | "sass-loader": "^13.0.0", 82 | "signale": "^1.4.0", 83 | "slash2": "^2.0.0", 84 | "typescript": "^4.7.4", 85 | "webpack": "^5.73.0" 86 | }, 87 | "dependencies": { 88 | "@react-spring/web": "^9.5.2", 89 | "antd-mobile": "^5.20.0", 90 | "antd-mobile-icons": "^0.3.0", 91 | "classnames": "^2.3.1", 92 | "react": "^18.2.0", 93 | "react-dom": "^18.2.0", 94 | "react-is": "^18.2.0", 95 | "react-transition-group": "^4.4.4" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoyage/react-mobile-ui/e0d17002078e43a0fdfc75407f78b3e937e4127d/packages/.DS_Store -------------------------------------------------------------------------------- /packages/action-sheet/actionSheet.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | import Popup from '@/popup'; 4 | 5 | import './styles/index.scss'; 6 | 7 | export interface Action { 8 | key: string | number; 9 | name: React.ReactNode; 10 | description?: React.ReactNode; 11 | disabled?: boolean; 12 | color?: string; 13 | onClick?: () => void; 14 | } 15 | 16 | export interface ActionSheetProps { 17 | visible: boolean; 18 | description?: React.ReactNode; 19 | actions: Action[]; 20 | onClose?: () => void; 21 | onAction?: (action: Action, index: number) => void; 22 | /** 展示取消按钮 */ 23 | cancelText?: React.ReactNode; 24 | /** 是否点击action后触发onClose回调 */ 25 | closeOnAction?: boolean; 26 | popupClassName?: string; 27 | } 28 | 29 | const classPrefix = `ygm-action-sheet`; 30 | 31 | const ActionSheet: React.FC = React.memo((props) => { 32 | const onClose = React.useCallback(() => { 33 | props.onClose?.(); 34 | }, [props.onClose]); 35 | 36 | const renderAction = React.useCallback( 37 | (action: Action, index: number) => { 38 | const onClick = () => { 39 | if (action.disabled) return; 40 | 41 | action.onClick?.(); 42 | props.onAction?.(action, index); 43 | 44 | if (props.closeOnAction) { 45 | props.onClose?.(); 46 | } 47 | }; 48 | 49 | return ( 50 |
58 |
{action.name}
59 | {action.description &&
{action.description}
} 60 |
61 | ); 62 | }, 63 | [props.onAction, props.onClose] 64 | ); 65 | 66 | return ( 67 | 73 |
74 | {props.description &&
{props.description}
} 75 | 76 |
{props.actions.map(renderAction)}
77 | 78 | {props.cancelText && ( 79 | <> 80 |
81 |
82 | {props.cancelText} 83 |
84 | 85 | )} 86 |
87 | 88 | ); 89 | }); 90 | 91 | ActionSheet.defaultProps = { 92 | description: '', 93 | cancelText: '', 94 | }; 95 | 96 | export default ActionSheet; 97 | -------------------------------------------------------------------------------- /packages/action-sheet/index.tsx: -------------------------------------------------------------------------------- 1 | import ActionSheet from '@/action-sheet/actionSheet'; 2 | 3 | export default ActionSheet; 4 | export type { ActionSheetProps } from '@/action-sheet/actionSheet'; 5 | export type { Action } from '@/action-sheet/actionSheet'; 6 | -------------------------------------------------------------------------------- /packages/action-sheet/method.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoyage/react-mobile-ui/e0d17002078e43a0fdfc75407f78b3e937e4127d/packages/action-sheet/method.tsx -------------------------------------------------------------------------------- /packages/action-sheet/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-action-sheet: 'ygm-action-sheet'; 2 | 3 | .#{$class-prefix-action-sheet} { 4 | --action-gap-height: 8px; 5 | --action-gap-background: #f7f8fa; 6 | display: flex; 7 | flex-direction: column; 8 | overflow: hidden; 9 | user-select: none; 10 | 11 | &-popup { 12 | border-top-left-radius: var(--ygm-radius-xxxl); 13 | border-top-right-radius: var(--ygm-radius-xxxl); 14 | overflow: hidden; 15 | } 16 | 17 | &-desc { 18 | position: relative; 19 | padding: var(--ygm-padding-xl) var(--ygm-padding-l); 20 | color: var(--ygm-color-weak); 21 | font-size: var(--ygm-font-size-m); 22 | text-align: center; 23 | 24 | &::after { 25 | position: absolute; 26 | box-sizing: border-box; 27 | content: ''; 28 | right: var(--ygm-padding-l); 29 | left: var(--ygm-padding-l); 30 | border-bottom: 1px solid var(--ygm-color-border); 31 | bottom: 0; 32 | transform: scaleY(0.5); 33 | } 34 | } 35 | 36 | &-action-list { 37 | flex: 1 auto; 38 | overflow-y: auto; 39 | } 40 | 41 | &-action-gap { 42 | height: var(--action-gap-height); 43 | background: var(--action-gap-background); 44 | } 45 | 46 | &-action-item { 47 | font-size: var(--ygm-font-size-l); 48 | box-sizing: border-box; 49 | padding: var(--ygm-padding-l); 50 | text-align: center; 51 | cursor: pointer; 52 | width: 100%; 53 | background-color: var(--ygm-color-background); 54 | 55 | &-desc { 56 | font-size: var(--ygm-font-size-s); 57 | color: var(--adm-color-weak); 58 | padding-top: var(--ygm-padding-s); 59 | line-height: 18px; 60 | } 61 | } 62 | 63 | &-action-disabled { 64 | cursor: not-allowed; 65 | color: var(--ygm-color-light); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import SpinnerLoading from '@/spinner-loading'; 5 | import { isPromise } from '@/utils/validate'; 6 | 7 | import './styles/index.scss'; 8 | 9 | export interface ButtonProps { 10 | color?: 'default' | 'primary' | 'success' | 'warning' | 'danger'; 11 | size?: 'small' | 'middle' | 'large'; 12 | shape?: 'default' | 'rounded' | 'rectangular'; 13 | fill?: 'solid' | 'outline' | 'none'; 14 | children?: React.ReactNode; 15 | className?: string; 16 | onClick?: (event: React.MouseEvent) => Promise | unknown; 17 | block?: boolean; 18 | disabled?: boolean; 19 | loading?: boolean | 'auto'; 20 | loadingIcon?: React.ReactNode; 21 | } 22 | 23 | const classPrefix = 'ygm-button'; 24 | 25 | const Button: React.FC = (props) => { 26 | const [innerLoading, setInnerLoading] = React.useState(false); 27 | const loading = props.loading === 'auto' ? innerLoading : props.loading; 28 | 29 | const onButtonClick = async (e: React.MouseEvent) => { 30 | if (!props.onClick) return; 31 | 32 | const promise = props.onClick(e); 33 | 34 | if (isPromise(promise)) { 35 | try { 36 | setInnerLoading(true); 37 | await promise; 38 | setInnerLoading(false); 39 | } catch (e) { 40 | setInnerLoading(false); 41 | throw e; 42 | } 43 | } 44 | }; 45 | 46 | return ( 47 |
62 | {loading ?
{props.loadingIcon}
: props.children} 63 |
64 | ); 65 | }; 66 | 67 | Button.defaultProps = { 68 | color: 'default', 69 | size: 'middle', 70 | shape: 'default', 71 | fill: 'solid', 72 | loading: false, 73 | loadingIcon: , 74 | }; 75 | 76 | Button.displayName = 'Button'; 77 | 78 | export default Button; 79 | -------------------------------------------------------------------------------- /packages/button/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-button: 'ygm-button'; 2 | 3 | .#{$class-prefix-button} { 4 | --base-button-padding: 7px 12px; 5 | --small-button-padding: 3px 12px; 6 | --middle-button-padding: var(--base-button-padding); 7 | --large-button-padding: 11px 12px; 8 | 9 | --color: var(--ygm-color-white); 10 | --text-color: var(--ygm-color-text); 11 | --background-color: var(--ygm-color-background); 12 | --border-color: var(--ygm-color-border); 13 | --border-width: 1px; 14 | 15 | color: var(--text-color); 16 | background-color: var(--background-color); 17 | font-size: var(--ygm-font-size-l); 18 | position: relative; 19 | display: inline-block; 20 | box-sizing: border-box; 21 | padding: var(--base-button-padding); 22 | margin: 0; 23 | line-height: 1.4; 24 | text-align: center; 25 | border-radius: var(--ygm-radius-xs); 26 | user-select: none; 27 | border: var(--border-width) solid var(--border-color); 28 | 29 | &-block { 30 | display: block; 31 | width: 100%; 32 | } 33 | 34 | &-default { 35 | &.#{$class-prefix-button}-fill-outline { 36 | --background-color: transparent; 37 | --border-color: var(--ygm-color-text); 38 | } 39 | 40 | &.#{$class-prefix-button}-fill-none { 41 | --background-color: transparent; 42 | --border-width: 0px; 43 | } 44 | } 45 | 46 | &:not(&-default) { 47 | --text-color: var(--ygm-color-white); 48 | --background-color: var(--color); 49 | --border-color: var(--color); 50 | &.#{$class-prefix-button}-fill-outline { 51 | --text-color: var(--color); 52 | --background-color: transparent; 53 | } 54 | &.#{$class-prefix-button}-fill-none { 55 | --text-color: var(--color); 56 | --background-color: transparent; 57 | --border-width: 0px; 58 | } 59 | } 60 | 61 | &-primary { 62 | --color: var(--ygm-color-primary); 63 | } 64 | &-success { 65 | --color: var(--ygm-color-success); 66 | } 67 | &-danger { 68 | --color: var(--ygm-color-danger); 69 | } 70 | &-warning { 71 | --color: var(--ygm-color-warning); 72 | } 73 | 74 | &-small { 75 | padding: var(--small-button-padding); 76 | font-size: var(--ygm-font-size-m); 77 | } 78 | 79 | &-middle { 80 | padding: var(--middle-button-padding); 81 | } 82 | 83 | &-large { 84 | padding: var(--large-button-padding); 85 | font-size: var(--ygm-font-size-xl); 86 | } 87 | 88 | &-shape-rounded { 89 | border-radius: 1000px; 90 | } 91 | 92 | &-shape-rectangular { 93 | border-radius: 0; 94 | } 95 | 96 | &-loading-wrap { 97 | display: flex; 98 | height: 1.4em; 99 | align-items: center; 100 | justify-content: center; 101 | > .ygm-spinner-loading { 102 | opacity: 0.6; 103 | } 104 | } 105 | 106 | &-disabled { 107 | cursor: not-allowed; 108 | opacity: 0.4; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /packages/card/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import './styles/index.scss'; 5 | 6 | export interface CardProps { 7 | title?: React.ReactNode; 8 | extra?: React.ReactNode; 9 | headerClassName?: string; 10 | titleClassName?: string; 11 | extraClassName?: string; 12 | bodyClassName?: string; 13 | children?: React.ReactNode; 14 | onHeaderClick?: (event: React.MouseEvent) => void; 15 | onBodyClick?: (event: React.MouseEvent) => void; 16 | } 17 | 18 | const Card: React.FC = React.memo((props) => { 19 | const renderHeader = React.useCallback(() => { 20 | if (!(props.title || props.extra)) { 21 | return null; 22 | } 23 | 24 | return ( 25 |
26 |
{props.title}
27 |
{props.extra}
28 |
29 | ); 30 | }, [ 31 | props.extra, 32 | props.extraClassName, 33 | props.headerClassName, 34 | props.title, 35 | props.titleClassName, 36 | props.onHeaderClick, 37 | ]); 38 | 39 | const renderBody = React.useCallback(() => { 40 | if (!props.children) { 41 | return null; 42 | } 43 | 44 | return ( 45 |
52 | {props.children} 53 |
54 | ); 55 | }, [props.bodyClassName, props.children, props.extra, props.onBodyClick, props.title]); 56 | 57 | return ( 58 |
59 | {renderHeader()} 60 | {renderBody()} 61 |
62 | ); 63 | }); 64 | 65 | export default Card; 66 | 67 | Card.displayName = 'Card'; 68 | -------------------------------------------------------------------------------- /packages/card/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-card: 'ygm-card'; 2 | 3 | .#{$class-prefix-card} { 4 | --card-header-padding: 13px; 5 | --card-body-padding: 0 13px 13px 13px; 6 | 7 | background: var(--ygm-color-white); 8 | border-radius: var(--ygm-radius-xl); 9 | overflow: hidden; 10 | transform: rotate(0deg); 11 | 12 | &-header { 13 | position: relative; 14 | display: flex; 15 | justify-content: space-between; 16 | align-items: center; 17 | box-sizing: border-box; 18 | padding: var(--card-header-padding); 19 | 20 | &-title { 21 | font-size: var(--ygm-font-size-xl); 22 | } 23 | 24 | &-extra { 25 | font-size: var(--ygm-font-size-m); 26 | } 27 | } 28 | 29 | &-body { 30 | position: relative; 31 | padding: var(--card-body-padding); 32 | box-sizing: border-box; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/cell/cell-group.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import './styles/cell-group.scss'; 5 | 6 | export interface CellGroupProps { 7 | /** 标题 */ 8 | title?: React.ReactNode; 9 | /** 是否显示外边框 */ 10 | border?: boolean; 11 | /** card为展示成圆角的卡片形式 */ 12 | mode?: 'default' | 'card'; 13 | children?: React.ReactNode; 14 | style?: React.CSSProperties & 15 | Partial< 16 | Record< 17 | | '--cell-group-background' 18 | | '--cell-group-title-padding' 19 | | '--cell-group-title-font-size' 20 | | '--cell-group-title-line-height' 21 | | '--cell-group-card-padding' 22 | | '--cell-group-card-border-radius', 23 | string 24 | > 25 | >; 26 | } 27 | 28 | const classPrefix = 'ygm-cell-group'; 29 | 30 | const CellGroup: React.FC = (props) => { 31 | const renderTitle = () => { 32 | return props.title &&

{props.title}

; 33 | }; 34 | 35 | const renderContent = () => { 36 | return
{props.children}
; 37 | }; 38 | 39 | return ( 40 |
41 | {renderTitle()} 42 | {renderContent()} 43 |
44 | ); 45 | }; 46 | 47 | CellGroup.defaultProps = { 48 | mode: 'default', 49 | }; 50 | 51 | CellGroup.displayName = 'CellGroup'; 52 | 53 | export default CellGroup; 54 | -------------------------------------------------------------------------------- /packages/cell/cell.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | import { RightOutline } from 'antd-mobile-icons'; 4 | 5 | import './styles/cell.scss'; 6 | 7 | export interface CellProps { 8 | title?: React.ReactNode; 9 | description?: React.ReactNode; 10 | children?: React.ReactNode; 11 | leftIcon?: React.ReactNode; 12 | rightIcon?: React.ReactNode; 13 | /** 是否显示点击效果 */ 14 | clickable?: boolean; 15 | onClick?: (e: React.MouseEvent) => void; 16 | style?: React.CSSProperties & 17 | Partial>; 18 | } 19 | 20 | const classPrefix = 'ygm-cell'; 21 | 22 | const Cell: React.FC = (props) => { 23 | const clickable = props.clickable ?? !!props.onClick; 24 | 25 | const renderLeftIcon = () => { 26 | return props.leftIcon &&
{props.leftIcon}
; 27 | }; 28 | 29 | const renderRightIcon = () => { 30 | if (props.rightIcon) { 31 | return props.rightIcon; 32 | } 33 | 34 | if (clickable) { 35 | return ( 36 |
37 | 38 |
39 | ); 40 | } 41 | return null; 42 | }; 43 | 44 | const renderCellTitle = () => { 45 | return ( 46 | props.title && ( 47 |
48 | {props.title} 49 | {props.description &&
{props.description}
} 50 |
51 | ) 52 | ); 53 | }; 54 | 55 | const renderCellValue = () => { 56 | return props.children &&
{props.children}
; 57 | }; 58 | 59 | return ( 60 |
65 | {renderLeftIcon()} 66 | {renderCellTitle()} 67 | {renderCellValue()} 68 | {renderRightIcon()} 69 |
70 | ); 71 | }; 72 | 73 | Cell.defaultProps = { 74 | title: '', 75 | description: '', 76 | }; 77 | 78 | Cell.displayName = 'Cell'; 79 | 80 | export default Cell; 81 | -------------------------------------------------------------------------------- /packages/cell/index.tsx: -------------------------------------------------------------------------------- 1 | import CellGroup from '@/cell/cell-group'; 2 | import InternalCell from '@/cell/cell'; 3 | 4 | export type { CellGroupProps } from '@/cell/cell-group'; 5 | export type { CellProps } from '@/cell/cell'; 6 | 7 | type InternalCellType = typeof InternalCell; 8 | 9 | interface CellInterface extends InternalCellType { 10 | Group: typeof CellGroup; 11 | } 12 | 13 | const Cell = InternalCell as CellInterface; 14 | 15 | Cell.Group = CellGroup; 16 | 17 | export default Cell; 18 | -------------------------------------------------------------------------------- /packages/cell/styles/cell-group.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-cell-group: 'ygm-cell-group'; 2 | 3 | .#{$class-prefix-cell-group} { 4 | --cell-group-background: var(--ygm-color-background); 5 | --cell-group-title-padding: var(--ygm-padding-s) var(--ygm-padding-l); 6 | --cell-group-title-font-size: var(--ygm-font-size-m); 7 | --cell-group-title-line-height: 16px; 8 | 9 | --cell-group-card-padding: 0 var(--ygm-padding-l); 10 | --cell-group-card-border-radius: var(--ygm-radius-m); 11 | 12 | &-title { 13 | padding: var(--cell-group-title-padding); 14 | color: var(--ygm-color-weak); 15 | font-size: var(--cell-group-title-font-size); 16 | line-height: var(--cell-group-title-line-height); 17 | } 18 | 19 | &-content { 20 | background: var(--cell-group-background); 21 | overflow: hidden; 22 | } 23 | 24 | &-card { 25 | margin: var(--cell-group-card-padding); 26 | 27 | .#{$class-prefix-cell-group}-content { 28 | border-radius: var(--cell-group-card-border-radius); 29 | } 30 | 31 | .#{$class-prefix-cell-group}-title { 32 | padding-left: 0; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/cell/styles/cell.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-cell: 'ygm-cell'; 2 | 3 | .#{$class-prefix-cell} { 4 | --cell-padding: var(--ygm-padding-s) var(--ygm-padding-l); 5 | --cell-font-size: var(--ygm-font-size-m); 6 | --cell-color-background: var(--ygm-color-background); 7 | --cell-border-color: var(--ygm-color-border); 8 | 9 | position: relative; 10 | display: flex; 11 | box-sizing: border-box; 12 | width: 100%; 13 | padding: var(--cell-padding); 14 | overflow: hidden; 15 | color: var(--ygm-color-text); 16 | font-size: var(--cell-font-size); 17 | line-height: 24px; 18 | background: var(--cell-color-background); 19 | 20 | &-left-icon { 21 | margin-right: 4px; 22 | } 23 | 24 | &-right-icon { 25 | margin-left: 4px; 26 | color: var(--ygm-color-weak); 27 | } 28 | 29 | &-title { 30 | flex: 1; 31 | } 32 | 33 | &-desc { 34 | margin-top: 4px; 35 | color: var(--ygm-color-weak); 36 | font-size: var(--ygm-font-size-s); 37 | line-height: 18px; 38 | } 39 | 40 | &-value { 41 | color: var(--ygm-color-weak); 42 | text-align: right; 43 | overflow: hidden; 44 | position: relative; 45 | vertical-align: middle; 46 | word-wrap: break-word; 47 | } 48 | 49 | &:after { 50 | position: absolute; 51 | box-sizing: border-box; 52 | content: ' '; 53 | pointer-events: none; 54 | right: var(--ygm-padding-l); 55 | bottom: 0; 56 | left: var(--ygm-padding-l); 57 | border-bottom: 1px solid var(--cell-border-color); 58 | transform: scaleY(0.5); 59 | } 60 | 61 | &:last-child::after { 62 | display: none; 63 | } 64 | 65 | &-clickable { 66 | cursor: pointer; 67 | 68 | &:active { 69 | background-color: var(--adm-border-color); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/countdown/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect'; 4 | import { getTimeItems } from '@/countdown/utils'; 5 | 6 | import './styles/index.scss'; 7 | 8 | export interface CountdownProps { 9 | /** 倒计时总时长,单位毫秒 */ 10 | time: number; 11 | /** 倒计时格式 */ 12 | format?: string; 13 | /** 结束文案 */ 14 | endText?: string; 15 | /** 数字样式 */ 16 | numberClassName?: string; 17 | /** 符号样式 */ 18 | symbolClassName?: string; 19 | /** 结束文案样式 */ 20 | endTextClassName?: string; 21 | } 22 | 23 | type timeItemType = { 24 | num: string; 25 | symbol: string; 26 | }[]; 27 | 28 | const Countdown: React.FC = React.memo((props) => { 29 | const [timeItems, setTimeItems] = React.useState([]); 30 | const [timeEnd, setTimeEnd] = React.useState(false); 31 | const computeTimeRef = React.useRef(props.time); 32 | const timerRef = React.useRef(0); 33 | const endTimeMs = React.useMemo(() => Date.now() + computeTimeRef.current, []); 34 | 35 | const setCountdownTimeItems = React.useCallback(() => { 36 | if (computeTimeRef.current <= 0) { 37 | setTimeEnd(true); 38 | clearTimeout(timerRef.current); 39 | } 40 | 41 | const timeItems = getTimeItems(props.format!, computeTimeRef.current); 42 | setTimeItems(timeItems); 43 | }, [props.format]); 44 | 45 | const initCountdown = React.useCallback(() => { 46 | clearTimeout(timerRef.current); 47 | // 当前时间 48 | const now = Date.now(); 49 | // 获得剩余毫秒数 50 | computeTimeRef.current = endTimeMs - now; 51 | timerRef.current = window.setTimeout(() => { 52 | initCountdown(); 53 | }); 54 | setCountdownTimeItems(); 55 | }, [endTimeMs, setCountdownTimeItems]); 56 | 57 | useIsomorphicLayoutEffect(() => { 58 | initCountdown(); 59 | 60 | return () => clearTimeout(timerRef.current); 61 | }, [initCountdown]); 62 | 63 | return ( 64 |
65 | {timeEnd && props.endText ? ( 66 |
{props.endText}
67 | ) : ( 68 | timeItems.map((item, index) => ( 69 |
70 |
{item.num}
71 |
{item.symbol}
72 |
73 | )) 74 | )} 75 |
76 | ); 77 | }); 78 | 79 | Countdown.displayName = 'Countdown'; 80 | 81 | Countdown.defaultProps = { 82 | format: 'hh:mm:ss', 83 | }; 84 | 85 | export default Countdown; 86 | -------------------------------------------------------------------------------- /packages/countdown/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-countdown: 'ygm-countdown'; 2 | 3 | .#{$class-prefix-countdown} { 4 | display: flex; 5 | align-items: center; 6 | font-size: var(--ygm-font-size-m); 7 | 8 | &-item { 9 | display: flex; 10 | align-items: center; 11 | font-size: var(--ygm-font-size-s); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/countdown/utils.ts: -------------------------------------------------------------------------------- 1 | const DAY_MILLISECONDS = 24 * 60 * 60 * 1000; // 一天毫秒数 2 | const HOURS_MILLISECONDS = 60 * 60 * 1000; // 小时毫秒 3 | const MINUTES_MILLISECONDS = 60 * 1000; // 分钟毫秒 4 | 5 | const formatTime = (val: number): string => { 6 | if (val <= 0) return '00'; 7 | return val < 10 ? `0${val}` : `${val}`; 8 | }; 9 | 10 | const getTime = (format: string, timeLeft: number) => { 11 | let d = timeLeft; 12 | let [_, s, m, h] = [1000, 60, 60, 24].map((unit) => { 13 | let num = d % unit; 14 | d = Math.floor(d / unit); 15 | return num; 16 | }); 17 | 18 | // [1毫秒,3秒,0, 0] 19 | 20 | if (timeLeft > DAY_MILLISECONDS && format.indexOf('d') === -1) { 21 | h += d * 24; 22 | } 23 | 24 | if (timeLeft > HOURS_MILLISECONDS && format.indexOf('h') === -1) { 25 | m += h * 60; 26 | } 27 | 28 | if (timeLeft > MINUTES_MILLISECONDS && format.indexOf('m') === -1) { 29 | s += m * 60; 30 | } 31 | 32 | return { 33 | dd: formatTime(d), 34 | hh: formatTime(h), 35 | mm: formatTime(m), 36 | ss: formatTime(s), 37 | d, 38 | h, 39 | m, 40 | s, 41 | }; 42 | }; 43 | 44 | type formatType = 'dd' | 'hh' | 'mm' | 'ss'; 45 | 46 | export const getTimeItems = (format: string, timeLeft: number) => { 47 | // 匹配format 48 | const timeArr: Array = format!.match(/[a-zA-Z]{1,3}/g) || []; 49 | // 匹配字符 50 | let symbolArr = format.match(/[\u4e00-\u9fa5]+|[^a-zA-Z]/g) || []; 51 | 52 | const time = getTime(format, timeLeft); 53 | 54 | return timeArr.map((item, i) => { 55 | return { 56 | num: time[item.toLowerCase() as formatType], 57 | symbol: symbolArr[i], 58 | }; 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /packages/dialog/alert.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { DialogProps } from '@/dialog/dialog'; 4 | import show from '@/dialog/show'; 5 | 6 | export type DialogAlertProps = Omit & { 7 | confirmText?: React.ReactNode; 8 | onConfirm?: () => void | Promise; 9 | }; 10 | 11 | const alert = (props: DialogAlertProps) => { 12 | const { confirmText = '确认' } = props; 13 | 14 | return new Promise((resolve) => { 15 | show({ 16 | ...props, 17 | closeOnAction: true, 18 | actions: [ 19 | { 20 | key: 'confirm', 21 | text: confirmText, 22 | color: 'primary', 23 | }, 24 | ], 25 | onAction: props.onConfirm, 26 | onClose: () => { 27 | props.onClose?.(); 28 | resolve(); 29 | }, 30 | }); 31 | }); 32 | }; 33 | 34 | export default alert; 35 | -------------------------------------------------------------------------------- /packages/dialog/confirm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { DialogProps } from '@/dialog/dialog'; 4 | import show from '@/dialog/show'; 5 | 6 | export type DialogConfirmProps = Omit & { 7 | confirmText?: React.ReactNode; 8 | cancelText?: React.ReactNode; 9 | onConfirm?: () => void | Promise; 10 | onCancel?: () => void | Promise; 11 | onClose?: () => void; 12 | }; 13 | 14 | const confirm = (props: DialogConfirmProps) => { 15 | const { confirmText = '确定', cancelText = '取消' } = props; 16 | 17 | return new Promise((resolve) => { 18 | show({ 19 | ...props, 20 | closeOnAction: true, 21 | actions: [ 22 | { 23 | key: 'cancel', 24 | text: cancelText, 25 | onClick: async () => { 26 | await props.onCancel?.(); 27 | resolve(false); 28 | }, 29 | }, 30 | { 31 | key: 'confirm', 32 | text: confirmText, 33 | color: 'primary', 34 | onClick: async () => { 35 | await props.onConfirm?.(); 36 | resolve(true); 37 | }, 38 | }, 39 | ], 40 | }); 41 | }); 42 | }; 43 | 44 | export default confirm; 45 | -------------------------------------------------------------------------------- /packages/dialog/dialog-action-button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@/button'; 3 | 4 | export interface Action { 5 | key: string; 6 | text: React.ReactNode; 7 | color?: 'danger' | 'primary' | 'default'; 8 | disabled?: boolean; 9 | onClick?: () => void | Promise; 10 | } 11 | 12 | interface DialogActionButtonProps { 13 | action: Action; 14 | onAction: () => void | Promise; 15 | } 16 | 17 | const classPrefix = 'ygm-dialog-button'; 18 | 19 | const DialogActionButton: React.FC = (props) => { 20 | return ( 21 | 34 | ); 35 | }; 36 | 37 | DialogActionButton.defaultProps = {}; 38 | 39 | DialogActionButton.displayName = 'DialogActionButton'; 40 | 41 | export default DialogActionButton; 42 | -------------------------------------------------------------------------------- /packages/dialog/dialog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSpring, animated } from '@react-spring/web'; 3 | 4 | import Mask, { MaskProps } from '@/mask'; 5 | import DialogActionButton, { Action } from '@/dialog/dialog-action-button'; 6 | 7 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect'; 8 | 9 | import './styles/index.scss'; 10 | 11 | export interface DialogProps { 12 | header?: React.ReactNode; 13 | title?: React.ReactNode; 14 | content?: React.ReactNode; 15 | visible?: boolean; 16 | actions?: Action[]; 17 | maskStyle?: MaskProps['style']; 18 | /** 点击action后是否关闭 */ 19 | closeOnAction?: boolean; 20 | /** Dialog关闭时的回调 */ 21 | onClose?: () => void; 22 | /** 显示后回调 */ 23 | afterShow?: () => void; 24 | /** 关闭后回调 */ 25 | afterClose?: () => void; 26 | /** 点击action后回调 */ 27 | onAction?: (action: Action, index: number) => void | Promise; 28 | } 29 | 30 | const classPrefix = 'ygm-dialog'; 31 | 32 | const Dialog: React.FC = (props) => { 33 | const [active, setActive] = React.useState(props.visible!); 34 | 35 | const style = useSpring({ 36 | scale: props.visible ? 1 : 0.8, 37 | opacity: props.visible ? 1 : 0, 38 | config: { 39 | mass: 2.2, 40 | tension: 200, 41 | friction: 25, 42 | clamp: true, 43 | }, 44 | onRest: () => { 45 | setActive(props.visible!); 46 | if (props.visible) { 47 | props.afterShow?.(); 48 | } else { 49 | props.afterClose?.(); 50 | } 51 | }, 52 | }); 53 | 54 | useIsomorphicLayoutEffect(() => { 55 | if (props.visible) { 56 | setActive(true); 57 | } 58 | }, [props.visible]); 59 | 60 | const renderTitle = () => { 61 | if (props.title) { 62 | return
{props.title}
; 63 | } 64 | return null; 65 | }; 66 | 67 | const renderContent = () => { 68 | if (props.content) { 69 | return ( 70 |
71 |
{props.content}
72 |
73 | ); 74 | } 75 | return null; 76 | }; 77 | 78 | const renderFooter = () => { 79 | return ( 80 |
81 | {props.actions!.map((action, index) => ( 82 | { 86 | await Promise.all([action.onClick?.(), props.onAction?.(action, index)]); 87 | if (props.closeOnAction) { 88 | props.onClose?.(); 89 | } 90 | }} 91 | /> 92 | ))} 93 |
94 | ); 95 | }; 96 | 97 | return ( 98 |
99 | 100 |
101 | 102 |
103 | {renderTitle()} 104 | {renderContent()} 105 | {renderFooter()} 106 |
107 |
108 |
109 |
110 | ); 111 | }; 112 | 113 | Dialog.defaultProps = { 114 | visible: false, 115 | actions: [] as Action[], 116 | }; 117 | 118 | Dialog.displayName = 'Dialog'; 119 | 120 | export default Dialog; 121 | -------------------------------------------------------------------------------- /packages/dialog/index.tsx: -------------------------------------------------------------------------------- 1 | import InternalDialog from '@/dialog/dialog'; 2 | import alert from '@/dialog/alert'; 3 | import confirm from '@/dialog/confirm'; 4 | 5 | export type { DialogProps } from '@/dialog/dialog'; 6 | export type { DialogAlertProps } from '@/dialog/alert'; 7 | 8 | type InternalDialogType = typeof InternalDialog; 9 | 10 | export interface DialogInterface extends InternalDialogType { 11 | alert: typeof alert; 12 | confirm: typeof confirm; 13 | } 14 | 15 | const Dialog = InternalDialog as DialogInterface; 16 | 17 | Dialog.alert = alert; 18 | Dialog.confirm = confirm; 19 | 20 | export default Dialog; 21 | -------------------------------------------------------------------------------- /packages/dialog/show.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Dialog, { DialogProps } from '@/dialog/dialog'; 4 | import renderImperatively from '@/utils/render-imperatively'; 5 | 6 | export type DialogShowProps = Omit; 7 | 8 | function show(props: DialogShowProps) { 9 | const handler = renderImperatively(); 10 | } 11 | 12 | export default show; 13 | -------------------------------------------------------------------------------- /packages/dialog/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-dialog: 'ygm-dialog'; 2 | 3 | .#{$class-prefix-dialog} { 4 | --z-index: 1000; 5 | --max-width: 75vw; 6 | --min-width: 280px; 7 | --border-radius: var(--ygm-radius-xxxl); 8 | --background-color: var(--ygm-color-background); 9 | 10 | z-index: var(--z-index); 11 | position: fixed; 12 | 13 | &-wrap { 14 | position: fixed; 15 | z-index: calc(var(--z-index) + 10); 16 | top: 50%; 17 | left: 50%; 18 | width: auto; 19 | min-width: var(--min-width); 20 | max-width: var(--max-width); 21 | transform: translate3d(-50%, -50%, 0); 22 | } 23 | 24 | &-body { 25 | background-color: var(--background-color); 26 | border-radius: var(--border-radius); 27 | overflow: hidden; 28 | } 29 | 30 | &-header { 31 | padding: var(--ygm-padding-xxl) 12px 0; 32 | font-weight: 500; 33 | line-height: 25px; 34 | text-align: center; 35 | font-size: var(--ygm-font-size-l); 36 | } 37 | 38 | &-content { 39 | width: auto; 40 | padding: 26px 12px; 41 | max-height: 70vh; 42 | overflow-x: hidden; 43 | overflow-y: auto; 44 | font-size: var(--ygm-font-size-m); 45 | line-height: 20px; 46 | color: var(--ygm-color-text); 47 | display: flex; 48 | justify-content: center; 49 | text-align: center; 50 | } 51 | 52 | &-footer { 53 | user-select: none; 54 | display: flex; 55 | align-items: stretch; 56 | border-top: 0.5px solid var(--ygm-color-border); 57 | 58 | > .ygm-dialog-button { 59 | flex: 1; 60 | padding: 10px; 61 | border-radius: 0; 62 | font-size: var(--ygm-font-size-m); 63 | border-right: solid 0.5px var(--ygm-color-border); 64 | line-height: 25px; 65 | } 66 | 67 | &:last-child { 68 | border-right: none; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/divider/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import './styles/index.scss'; 5 | 6 | export interface DividerProps { 7 | contentPosition?: 'left' | 'right' | 'center'; 8 | /** 是否使用虚线 */ 9 | dashed?: boolean; 10 | /** 水平还是垂直类型 */ 11 | direction?: 'horizontal' | 'vertical'; 12 | /** 是否使用 0.5px 线 */ 13 | hairline?: boolean; 14 | children?: React.ReactNode; 15 | style?: React.CSSProperties & 16 | Partial>; 17 | } 18 | 19 | const classPrefix = 'ygm-divider'; 20 | 21 | const Divider: React.FC = (props) => { 22 | return ( 23 |
30 | {props.children &&
{props.children}
} 31 |
32 | ); 33 | }; 34 | 35 | export default Divider; 36 | 37 | Divider.defaultProps = { 38 | contentPosition: 'center', 39 | direction: 'horizontal', 40 | hairline: true, 41 | }; 42 | -------------------------------------------------------------------------------- /packages/divider/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-divider: 'ygm-divider'; 2 | 3 | .#{$class-prefix-divider} { 4 | --border-width: 1px; 5 | --border-padding: var(--ygm-padding-l) 0; 6 | --text-color: var(--ygm-color-weak); 7 | --border-color: var(--ygm-color-border); 8 | 9 | display: flex; 10 | align-items: center; 11 | border-color: var(--border-color); 12 | color: var(--text-color); 13 | font-size: var(--ygm-font-size-m); 14 | border-style: solid; 15 | line-height: 24px; 16 | margin: var(--border-padding); 17 | 18 | &::before, 19 | &::after { 20 | content: ''; 21 | display: block; 22 | flex: 1; 23 | box-sizing: border-box; 24 | height: 1px; 25 | border-color: inherit; 26 | border-style: inherit; 27 | border-width: var(--border-width) 0 0 0; 28 | } 29 | 30 | &-hairline { 31 | &::before, 32 | &::after { 33 | transform: scaleY(0.5); 34 | } 35 | } 36 | 37 | &-vertical { 38 | position: relative; 39 | top: -0.06em; 40 | display: inline-block; 41 | height: 0.9em; 42 | vertical-align: middle; 43 | border-top: 0; 44 | margin: 0; 45 | margin: 0 16px; 46 | border-left-width: var(--border-width); 47 | 48 | &::before, 49 | &::after { 50 | content: none !important; 51 | } 52 | } 53 | 54 | &-dashed { 55 | border-style: dashed; 56 | } 57 | 58 | &-content { 59 | padding: 0 var(--ygm-padding-l); 60 | } 61 | 62 | &-left { 63 | &::before { 64 | max-width: 10%; 65 | } 66 | } 67 | 68 | &-right { 69 | &::after { 70 | max-width: 10%; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/ellipsis/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect'; 3 | import useResizeObserver from '@/hooks/useResizeObserver'; 4 | import { pxToNumber } from '@/ellipsis/utils'; 5 | 6 | import './styles/index.scss'; 7 | 8 | export interface EllipsisProps { 9 | text: string; 10 | rows?: number; 11 | /** 收起 */ 12 | collapse?: React.ReactNode; 13 | /** 展开 */ 14 | expand?: React.ReactNode; 15 | } 16 | 17 | const classPrefix = 'ygm-ellipsis'; 18 | 19 | const ellipsisTailing = '...'; 20 | 21 | const Ellipsis: React.FC = (props) => { 22 | const [exceeded, setExceeded] = React.useState(false); 23 | const [expanded, setExpanded] = React.useState(false); 24 | const [ellipsised, setEllipsised] = React.useState(''); 25 | const containerRef = React.useRef(null); 26 | 27 | const calcEllipsised = React.useCallback(() => { 28 | const element = containerRef.current; 29 | if (!element) return; 30 | 31 | const originStyle = window.getComputedStyle(element); 32 | const container = document.createElement('div'); 33 | 34 | const styleNames: string[] = Array.prototype.slice.apply(originStyle); 35 | styleNames.forEach((name) => { 36 | container.style.setProperty(name, originStyle.getPropertyValue(name)); 37 | }); 38 | 39 | container.style.position = 'fixed'; 40 | container.style.height = 'auto'; 41 | container.style.visibility = 'hidden'; 42 | 43 | container.innerText = props.text; 44 | 45 | document.body.appendChild(container); 46 | 47 | const lineHeight = pxToNumber(originStyle.lineHeight); 48 | const maxHeight = lineHeight * props.rows!; 49 | const height = container.getBoundingClientRect().height; 50 | 51 | const check = (left: number, right: number) => { 52 | let l = left; 53 | let r = right; 54 | let text = ''; 55 | 56 | while (l < r) { 57 | const m = Math.floor((l + r) / 2); 58 | if (l === m) { 59 | break; 60 | } 61 | 62 | const tempText = props.text.slice(l, m); 63 | container.innerText = `${text}${tempText}...${props.expand}`; 64 | const height = container.getBoundingClientRect().height; 65 | 66 | if (height > maxHeight) { 67 | r = m; 68 | } else { 69 | text += tempText; 70 | l = m; 71 | } 72 | } 73 | 74 | return text; 75 | }; 76 | 77 | if (maxHeight >= height) { 78 | setExceeded(false); 79 | } else { 80 | setExceeded(true); 81 | const end = props.text.length; 82 | const ellipsisedValue = check(0, end); 83 | setEllipsised(ellipsisedValue); 84 | } 85 | document.body.removeChild(container); 86 | }, [props.expand, props.rows, props.text]); 87 | 88 | useIsomorphicLayoutEffect(() => { 89 | calcEllipsised(); 90 | }, [calcEllipsised]); 91 | 92 | useResizeObserver(calcEllipsised, containerRef); 93 | 94 | const renderContent = () => { 95 | if (!exceeded) { 96 | return props.text; 97 | } 98 | if (expanded) { 99 | return ( 100 | <> 101 | {props.text} 102 | {props.collapse && {props.collapse}} 103 | 104 | ); 105 | } else { 106 | return ( 107 | <> 108 | {ellipsised} 109 | {ellipsisTailing} 110 | {props.expand && {props.expand}} 111 | 112 | ); 113 | } 114 | }; 115 | 116 | const onContent = (e: React.MouseEvent) => { 117 | e.stopPropagation(); 118 | 119 | if (!props.expand && !props.collapse) return; 120 | 121 | if (props.expand && !props.collapse) { 122 | setExpanded(true); 123 | return; 124 | } 125 | 126 | setExpanded(!expanded); 127 | }; 128 | 129 | return ( 130 |
131 | {renderContent()} 132 |
133 | ); 134 | }; 135 | 136 | Ellipsis.defaultProps = { 137 | text: '', 138 | rows: 1, 139 | expand: '', 140 | collapse: '', 141 | }; 142 | 143 | Ellipsis.displayName = 'Ellipsis'; 144 | 145 | export default Ellipsis; 146 | -------------------------------------------------------------------------------- /packages/ellipsis/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-ellipsis: 'ygm-ellipsis'; 2 | 3 | .#{$class-prefix-ellipsis} { 4 | overflow: hidden; 5 | word-break: break-all; 6 | line-height: 1.5; 7 | cursor: pointer; 8 | } 9 | -------------------------------------------------------------------------------- /packages/ellipsis/utils.ts: -------------------------------------------------------------------------------- 1 | export const pxToNumber = (value: string) => { 2 | const num = parseFloat(value); 3 | return isNaN(num) ? 0 : num; 4 | }; 5 | -------------------------------------------------------------------------------- /packages/error-block/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ErrorImage from './errorImage'; 4 | 5 | import './styles/index.scss'; 6 | 7 | export interface ErrorBlockProps { 8 | title?: React.ReactNode; 9 | description?: React.ReactNode; 10 | image?: React.ReactNode; 11 | } 12 | 13 | const classPrefix = 'ygm-error-block'; 14 | 15 | const ErrorBlock: React.FC = React.memo((props) => { 16 | let imageNode: React.ReactNode = ErrorImage; 17 | 18 | if (props.image) { 19 | imageNode = props.image; 20 | } 21 | 22 | return ( 23 |
24 |
{imageNode}
25 |
26 | {props.title &&
{props.title}
} 27 | {props.description &&
{props.description}
} 28 |
29 |
30 | ); 31 | }); 32 | 33 | ErrorBlock.defaultProps = { 34 | title: '页面遇到一些小问题', 35 | description: '请稍后重试', 36 | }; 37 | 38 | ErrorBlock.displayName = 'ErrorBlock'; 39 | 40 | export default ErrorBlock; 41 | -------------------------------------------------------------------------------- /packages/error-block/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-error-block: 'ygm-error-block'; 2 | 3 | .#{$class-prefix-error-block} { 4 | --image-height: 200px; 5 | --image-width: 100%; 6 | 7 | box-sizing: border-box; 8 | text-align: center; 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | justify-content: center; 13 | height: 100vh; 14 | transform: translateY(-10%); 15 | 16 | &-image { 17 | height: var(--image-height); 18 | width: var(--image-width); 19 | 20 | & svg { 21 | height: 100%; 22 | } 23 | } 24 | 25 | &-description { 26 | font-size: var(--ygm-font-size-s); 27 | color: var(--ygm-color-weak); 28 | line-height: 1.4; 29 | margin-top: 10px; 30 | &-title { 31 | font-size: var(--ygm-font-size-m); 32 | } 33 | &-subtitle { 34 | margin-top: 8px; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/grid/grid-item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './styles/grid-item.scss'; 4 | 5 | export interface GridItemProps { 6 | span?: number; 7 | onClick?: (event: React.MouseEvent) => void; 8 | children: React.ReactNode; 9 | } 10 | 11 | const classPrefix = 'ygm-grid-item'; 12 | 13 | const GridItem: React.FC = React.memo((props) => { 14 | const style = React.useMemo(() => { 15 | return { 16 | '--item-span': props.span, 17 | }; 18 | }, [props.span]); 19 | 20 | return ( 21 |
22 | {props.children} 23 |
24 | ); 25 | }); 26 | 27 | export default GridItem; 28 | -------------------------------------------------------------------------------- /packages/grid/grid.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './styles/grid.scss'; 4 | 5 | export interface GridProps { 6 | columns: number; 7 | gap?: number | string | [number | string, number | string]; 8 | children?: React.ReactNode; 9 | } 10 | 11 | const formatGap = (gap: string | number) => (typeof gap === 'number' ? `${gap}px` : gap); 12 | 13 | const classPrefix = 'ygm-grid'; 14 | 15 | const Grid: React.FC = (props) => { 16 | const style = React.useMemo(() => { 17 | if (props.gap !== undefined) { 18 | if (Array.isArray(props.gap)) { 19 | const [gapH, gapV] = props.gap; 20 | return { 21 | '--gap-horizontal': formatGap(gapH), 22 | '--gap-vertical': formatGap(gapV), 23 | '--columns': props.columns, 24 | }; 25 | } else { 26 | return { '--gap': formatGap(props.gap), '--columns': props.columns }; 27 | } 28 | } 29 | return { '--columns': props.columns }; 30 | }, [props.gap, props.columns]); 31 | 32 | return ( 33 |
34 | {props.children} 35 |
36 | ); 37 | }; 38 | 39 | Grid.displayName = 'Grid'; 40 | 41 | export default Grid; 42 | -------------------------------------------------------------------------------- /packages/grid/index.tsx: -------------------------------------------------------------------------------- 1 | import InternalGrid from '@/grid/grid'; 2 | import GridItem from '@/grid/grid-item'; 3 | 4 | export type { GridProps } from '@/grid/grid'; 5 | export type { GridItemProps } from '@/grid/grid-item'; 6 | 7 | type InternalGridType = typeof InternalGrid; 8 | 9 | export interface GridInterface extends InternalGridType { 10 | Item: typeof GridItem; 11 | } 12 | 13 | const Grid = InternalGrid as GridInterface; 14 | 15 | Grid.Item = GridItem; 16 | 17 | export default Grid; 18 | -------------------------------------------------------------------------------- /packages/grid/styles/grid-item.scss: -------------------------------------------------------------------------------- 1 | .ygm-grid-item { 2 | grid-column-end: span var(--item-span); 3 | } 4 | -------------------------------------------------------------------------------- /packages/grid/styles/grid.scss: -------------------------------------------------------------------------------- 1 | .ygm-grid { 2 | --gap: 0; 3 | --gap-horizontal: var(--gap); 4 | --gap-vertical: var(--gap); 5 | 6 | display: grid; 7 | grid-gap: 10px; 8 | column-gap: var(--gap-horizontal); 9 | row-gap: var(--gap-vertical); 10 | grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); 11 | align-items: stretch; 12 | } 13 | -------------------------------------------------------------------------------- /packages/hooks/index.tsx: -------------------------------------------------------------------------------- 1 | import useMount from '@/hooks/useMount'; 2 | import useEffectOnce from '@/hooks/useEffectOnce'; 3 | import useIntersectionObserver from '@/hooks/useIntersectionObserver'; 4 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect'; 5 | import useScrollLock from '@/hooks/useScrollLock'; 6 | import useUpdateEffect from '@/hooks/useUpdateEffect'; 7 | import useUpdateIsomorphicLayoutEffect from '@/hooks/useUpdateIsomorphicLayoutEffect'; 8 | import useLockFn from '@/hooks/useLockFn'; 9 | import useLatest from '@/hooks/useLatest'; 10 | import useUnmount from '@/hooks/useUnmount'; 11 | import useResizeObserver from '@/hooks/useResizeObserver'; 12 | import useMemoizedFn from '@/hooks/useMemoizedFn'; 13 | 14 | export default { 15 | useMount, 16 | useEffectOnce, 17 | useIntersectionObserver, 18 | useIsomorphicLayoutEffect, 19 | useScrollLock, 20 | useUpdateEffect, 21 | useUpdateIsomorphicLayoutEffect, 22 | useLockFn, 23 | useLatest, 24 | useUnmount, 25 | useResizeObserver, 26 | useMemoizedFn, 27 | }; 28 | -------------------------------------------------------------------------------- /packages/hooks/useEffectOnce.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const useEffectOnce = (effect: React.EffectCallback) => { 4 | // eslint-disable-next-line react-hooks/exhaustive-deps 5 | React.useEffect(effect, []); 6 | }; 7 | 8 | export default useEffectOnce; 9 | -------------------------------------------------------------------------------- /packages/hooks/useIntersectionObserver.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface Options extends IntersectionObserverInit { 4 | freezeOnceVisible?: boolean; 5 | } 6 | 7 | const useIntersectionObserver = ( 8 | targetRef: React.RefObject, 9 | { threshold = 0, root = null, rootMargin = '0%', freezeOnceVisible = false }: Options 10 | ) => { 11 | const [entry, setEntry] = React.useState(); 12 | 13 | const frozen = entry?.isIntersecting && freezeOnceVisible; 14 | 15 | React.useEffect(() => { 16 | const element = targetRef.current; 17 | 18 | if (!element || frozen) return; 19 | 20 | const observerParams = { threshold, root, rootMargin }; 21 | 22 | const ob = new IntersectionObserver(([entry]: IntersectionObserverEntry[]) => { 23 | setEntry(entry); 24 | }, observerParams); 25 | 26 | ob.observe(element); 27 | 28 | return () => { 29 | ob.disconnect(); 30 | }; 31 | }, [frozen, root, rootMargin, targetRef, threshold]); 32 | 33 | return entry; 34 | }; 35 | 36 | export default useIntersectionObserver; 37 | -------------------------------------------------------------------------------- /packages/hooks/useIsomorphicLayoutEffect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; 4 | 5 | export default useIsomorphicLayoutEffect; 6 | -------------------------------------------------------------------------------- /packages/hooks/useLatest.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const useLatest = (value: T) => { 4 | const ref = React.useRef(value); 5 | ref.current = value; 6 | 7 | return ref; 8 | }; 9 | 10 | export default useLatest; 11 | -------------------------------------------------------------------------------- /packages/hooks/useLockFn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const useLockFn =

(fn: (...args: P) => Promise) => { 4 | const lockRef = React.useRef(false); 5 | 6 | return React.useCallback( 7 | async (...args: P) => { 8 | if (lockRef.current) return; 9 | 10 | lockRef.current = true; 11 | 12 | try { 13 | const ret = await fn(...args); 14 | lockRef.current = false; 15 | return ret; 16 | } catch (e) { 17 | lockRef.current = false; 18 | throw e; 19 | } 20 | }, 21 | [fn] 22 | ); 23 | }; 24 | 25 | export default useLockFn; 26 | -------------------------------------------------------------------------------- /packages/hooks/useMemoizedFn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import useLatest from '@/hooks/useLatest'; 4 | 5 | const useMemoizedFn = (fn: (...args: unknown[]) => void) => { 6 | const latestfnRef = useLatest(fn); 7 | 8 | const memoizedFn = React.useRef((...args: unknown[]) => { 9 | latestfnRef.current?.(...args); 10 | }); 11 | 12 | return memoizedFn.current; 13 | }; 14 | 15 | export default useMemoizedFn; 16 | -------------------------------------------------------------------------------- /packages/hooks/useMount.tsx: -------------------------------------------------------------------------------- 1 | import useEffectOnce from '@/hooks/useEffectOnce'; 2 | 3 | const useMount = (fn: () => void) => { 4 | useEffectOnce(() => { 5 | fn(); 6 | }); 7 | }; 8 | 9 | export default useMount; 10 | -------------------------------------------------------------------------------- /packages/hooks/useReadLocalStorage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Value = T | null; 4 | 5 | const useReadLocalStorage = (key: string): Value => { 6 | const readValue = React.useCallback((): Value => { 7 | if (typeof window === 'undefined') { 8 | return null; 9 | } 10 | 11 | try { 12 | const item = window.localStorage.getItem(key); 13 | return item ? (JSON.parse(item) as T) : null; 14 | } catch (err) { 15 | console.warn(`Error reading localStorage key "${key}":`, err); 16 | return null; 17 | } 18 | }, [key]); 19 | 20 | const [storedValue, setStoredValue] = React.useState>(readValue); 21 | 22 | const handleStorageChange = React.useCallback( 23 | (event: StorageEvent) => { 24 | if (event.key !== key) { 25 | return; 26 | } 27 | setStoredValue(readValue()); 28 | }, 29 | [key, readValue] 30 | ); 31 | 32 | React.useEffect(() => { 33 | window.addEventListener('storage', handleStorageChange); 34 | 35 | return () => { 36 | window.removeEventListener('storage', handleStorageChange); 37 | }; 38 | }, [handleStorageChange]); 39 | 40 | return storedValue; 41 | }; 42 | 43 | export default useReadLocalStorage; 44 | -------------------------------------------------------------------------------- /packages/hooks/useResizeObserver.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect'; 4 | 5 | const useResizeObserver = (callback: (target: T) => void, targetRef: React.RefObject) => { 6 | useIsomorphicLayoutEffect(() => { 7 | const element = targetRef.current; 8 | if (!element) return; 9 | 10 | if (window.ResizeObserver) { 11 | const observer = new ResizeObserver(() => { 12 | callback(element); 13 | }); 14 | observer.observe(element); 15 | return () => { 16 | observer.disconnect(); 17 | }; 18 | } 19 | callback(element); 20 | 21 | return () => null; 22 | }, []); 23 | }; 24 | 25 | export default useResizeObserver; 26 | -------------------------------------------------------------------------------- /packages/hooks/useScrollLock.tsx: -------------------------------------------------------------------------------- 1 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect'; 2 | 3 | const useScrollLock = (visible: boolean) => { 4 | useIsomorphicLayoutEffect(() => { 5 | if (!visible) return; 6 | 7 | const el = document.getElementsByTagName('html')[0]; 8 | el.style.overflow = 'hidden'; 9 | 10 | return () => { 11 | el.style.overflow = ''; 12 | }; 13 | }, [visible]); 14 | }; 15 | 16 | export default useScrollLock; 17 | -------------------------------------------------------------------------------- /packages/hooks/useThrottleFn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useLatest from '@/hooks/useLatest'; 3 | import useUnmount from '@/hooks/useUnmount'; 4 | 5 | const useThrottleFn = (fn: (...args: any[]) => any, ms: number) => { 6 | const timerRef = React.useRef>(); 7 | const fnRef = useLatest(fn); 8 | 9 | const timeoutCallback = React.useCallback(() => { 10 | fnRef.current(); 11 | timerRef.current = undefined; 12 | }, []); 13 | 14 | React.useEffect(() => { 15 | if (!timerRef.current) { 16 | timerRef.current = setTimeout(timeoutCallback, ms); 17 | } 18 | }, [ms, timeoutCallback]); 19 | 20 | useUnmount(() => { 21 | timerRef.current && clearTimeout(timerRef.current); 22 | }); 23 | 24 | return {}; 25 | }; 26 | 27 | export default useThrottleFn; 28 | -------------------------------------------------------------------------------- /packages/hooks/useUnmount.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useEffectOnce from '@/hooks/useEffectOnce'; 3 | 4 | const useUnmount = (fn: () => any) => { 5 | const fnRef = React.useRef(fn); 6 | 7 | fnRef.current = fn; 8 | 9 | useEffectOnce(() => () => fnRef.current()); 10 | }; 11 | 12 | export default useUnmount; 13 | -------------------------------------------------------------------------------- /packages/hooks/useUpdateEffect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const useUpdateEffect = (callback: React.EffectCallback, deep?: React.DependencyList) => { 4 | const isMounted = React.useRef(false); 5 | 6 | React.useEffect(() => { 7 | return () => { 8 | isMounted.current = false; 9 | }; 10 | }, []); 11 | 12 | React.useEffect(() => { 13 | if (!isMounted.current) { 14 | isMounted.current = true; 15 | } else { 16 | callback(); 17 | } 18 | // eslint-disable-next-line react-hooks/exhaustive-deps 19 | }, deep); 20 | }; 21 | 22 | export default useUpdateEffect; 23 | -------------------------------------------------------------------------------- /packages/hooks/useUpdateIsomorphicLayoutEffect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect'; 4 | 5 | const useUpdateIsomorphicLayoutEffect = (callback: React.EffectCallback, deep?: React.DependencyList) => { 6 | const isMounted = React.useRef(false); 7 | 8 | useIsomorphicLayoutEffect(() => { 9 | return () => { 10 | isMounted.current = false; 11 | }; 12 | }, []); 13 | 14 | useIsomorphicLayoutEffect(() => { 15 | if (!isMounted.current) { 16 | isMounted.current = true; 17 | } else { 18 | callback(); 19 | } 20 | }, deep); 21 | }; 22 | 23 | export default useUpdateIsomorphicLayoutEffect; 24 | -------------------------------------------------------------------------------- /packages/image/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useIntersectionObserver from '@/hooks/useIntersectionObserver'; 3 | 4 | export interface ImageProps { 5 | /** 图片地址 */ 6 | src: string; 7 | /** 图片描述 */ 8 | alt?: string; 9 | /** 图片宽度 */ 10 | width?: number | string; 11 | /** 图片高度 */ 12 | height?: number | string; 13 | /** 加载时的占位图地址 */ 14 | loading?: string; 15 | style?: React.CSSProperties; 16 | /** 是否懒加载 */ 17 | lazy?: boolean; 18 | /** 图片填充模式 */ 19 | fit?: 'contain' | 'cover' | 'fill' | 'scale-down'; 20 | className?: string; 21 | /** 图片点击事件 */ 22 | onClick?: (event: React.MouseEvent) => void; 23 | /** 图片加载失败时回调 */ 24 | onError?: (event: React.SyntheticEvent) => void; 25 | /** 图片加载完成时回调 */ 26 | onLoad?: (event: React.SyntheticEvent) => void; 27 | } 28 | 29 | const Image: React.FC = (props) => { 30 | const imageRef = React.useRef(null); 31 | const observerEntry = useIntersectionObserver(imageRef, { freezeOnceVisible: true }); 32 | 33 | return ( 34 | {props.alt} 47 | ); 48 | }; 49 | 50 | Image.defaultProps = { 51 | alt: '', 52 | width: '100%', 53 | height: '100%', 54 | lazy: false, 55 | fit: 'fill', 56 | loading: 57 | 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mM8/x8AAqMB0Fk+W34AAAAASUVORK5CYII=', 58 | }; 59 | 60 | Image.displayName = 'Image'; 61 | 62 | export default Image; 63 | -------------------------------------------------------------------------------- /packages/index.tsx: -------------------------------------------------------------------------------- 1 | import './styles/index.scss'; 2 | 3 | export { default as Button } from '@/button'; 4 | export type { ButtonProps } from '@/button'; 5 | 6 | export { default as Mask } from '@/mask'; 7 | export type { MaskProps } from '@/mask'; 8 | 9 | export { default as Popup } from '@/popup'; 10 | export type { PopupProps } from '@/popup'; 11 | 12 | export { default as Toast } from '@/toast'; 13 | export type { ToastProps, ToastShowProps } from '@/toast'; 14 | 15 | export { default as SpinnerLoading } from '@/spinner-loading'; 16 | export type { SpinnerLoadingProps } from '@/spinner-loading'; 17 | 18 | export { default as NavBar } from '@/nav-bar'; 19 | export type { NavBarProps } from '@/nav-bar'; 20 | 21 | export { default as Card } from '@/card'; 22 | export type { CardProps } from '@/card'; 23 | 24 | export { default as Image } from '@/image'; 25 | export type { ImageProps } from '@/image'; 26 | 27 | export { default as Countdown } from '@/countdown'; 28 | export type { CountdownProps } from '@/countdown'; 29 | 30 | export { default as PullToRefresh } from '@/pull-to-refresh'; 31 | export type { PullToRefreshProps } from '@/pull-to-refresh'; 32 | 33 | export { default as Space } from '@/space'; 34 | export type { SpaceProps } from '@/space'; 35 | 36 | export { default as Tabs } from '@/tabs'; 37 | export type { TabsProps, TabProps } from '@/tabs'; 38 | 39 | export { default as Swiper } from '@/swiper'; 40 | export type { SwiperProps, SwiperItemProps, SwiperRef } from '@/swiper'; 41 | 42 | export { default as Grid } from '@/grid'; 43 | export type { GridProps, GridItemProps } from '@/grid'; 44 | 45 | export type { ErrorBlockProps } from '@/error-block'; 46 | export { default as ErrorBlock } from '@/error-block'; 47 | 48 | export type { InputProps, InputRef } from '@/input'; 49 | export { default as Input } from '@/input'; 50 | 51 | export type { SidebarProps } from '@/sidebar'; 52 | export { default as Sidebar } from '@/sidebar'; 53 | 54 | export type { SearchBarProps, SearchBarRef } from '@/search-bar'; 55 | export { default as SearchBar } from '@/search-bar'; 56 | 57 | export type { ActionSheetProps, Action } from '@/action-sheet'; 58 | export { default as ActionSheet } from '@/action-sheet'; 59 | 60 | export type { InfiniteScrollProps } from '@/infinite-scroll'; 61 | export { default as InfiniteScroll } from '@/infinite-scroll'; 62 | 63 | export type { CellGroupProps, CellProps } from '@/cell'; 64 | export { default as Cell } from '@/cell'; 65 | 66 | export type { EllipsisProps } from '@/ellipsis'; 67 | export { default as Ellipsis } from '@/ellipsis'; 68 | 69 | export type { SelectorProps } from '@/selector'; 70 | export { default as Selector } from '@/selector'; 71 | 72 | export type { SliderProps, SliderRef } from '@/slider'; 73 | export { default as Slider } from '@/slider'; 74 | 75 | export type { DialogProps, DialogAlertProps } from '@/dialog'; 76 | export { default as Dialog } from '@/dialog'; 77 | 78 | export type { DividerProps } from '@/divider'; 79 | export { default as Divider } from '@/divider'; 80 | 81 | export { default as hooks } from '@/hooks'; 82 | -------------------------------------------------------------------------------- /packages/infinite-scroll/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Loading from '@/spinner-loading'; 3 | 4 | import useIntersectionObserver from '@/hooks/useIntersectionObserver'; 5 | import useLockFn from '@/hooks/useLockFn'; 6 | 7 | import './styles/index.scss'; 8 | 9 | export interface InfiniteScrollProps { 10 | /** 是否加载更多 */ 11 | hasMore: boolean; 12 | /** 加载数据方法 */ 13 | loadMore: () => Promise; 14 | /** 自定义底部样式 */ 15 | footer?: React.ReactNode; 16 | children: React.ReactNode; 17 | } 18 | 19 | const classPrefix = `ygm-infinite-scroll`; 20 | 21 | const InfiniteScroll: React.FC = React.memo((props) => { 22 | const doLoadMore = useLockFn(() => props.loadMore()); 23 | 24 | const intersectionEleRef = React.useRef(null); 25 | 26 | const observerEntry = useIntersectionObserver(intersectionEleRef, {}); 27 | 28 | const check = React.useCallback(async () => { 29 | if (!observerEntry?.isIntersecting) return; 30 | if (!props.hasMore) return; 31 | 32 | await doLoadMore(); 33 | }, [doLoadMore, observerEntry?.isIntersecting, props.hasMore]); 34 | 35 | React.useEffect(() => { 36 | check(); 37 | }, [check]); 38 | 39 | return ( 40 |

41 | {props.children} 42 | 43 |
44 | {props.footer && props.footer} 45 | {!props.footer && (props.hasMore ? : '')} 46 |
47 |
48 | ); 49 | }); 50 | 51 | InfiniteScroll.displayName = 'InfiniteScroll'; 52 | 53 | export default InfiniteScroll; 54 | -------------------------------------------------------------------------------- /packages/infinite-scroll/styles/index.scss: -------------------------------------------------------------------------------- 1 | .ygm-infinite-scroll { 2 | position: 'relative'; 3 | &-load { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | padding: 10px; 8 | font-size: var(--ygm-font-size-m); 9 | color: var(--ygm-color-weak); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/input/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import { CloseCircleFill } from 'antd-mobile-icons'; 5 | 6 | import './styles/index.scss'; 7 | 8 | type TStyle = Partial>; 9 | 10 | export interface InputRef { 11 | clear: () => void; 12 | focus: () => void; 13 | blur: () => void; 14 | setValue: (val: string) => void; 15 | } 16 | 17 | export interface InputProps { 18 | id?: string; 19 | value?: string; 20 | placeholder?: string; 21 | className?: string; 22 | /** 是否显示清除icon */ 23 | clearable?: boolean; 24 | style?: React.CSSProperties & TStyle; 25 | autoFocus?: boolean; 26 | disabled?: boolean; 27 | readOnly?: boolean; 28 | maxLength?: number; 29 | minLength?: number; 30 | max?: number; 31 | min?: number; 32 | pattern?: string; 33 | name?: string; 34 | autoComplete?: 'on' | 'off'; 35 | autoCapitalize?: 'on' | 'off'; 36 | autoCorrect?: 'on' | 'off'; 37 | inputMode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'; 38 | type?: React.HTMLInputTypeAttribute; 39 | onKeyDown?: React.KeyboardEventHandler; 40 | onKeyUp?: React.KeyboardEventHandler; 41 | onCompositionStart?: React.CompositionEventHandler; 42 | onCompositionEnd?: React.CompositionEventHandler; 43 | onClick?: React.MouseEventHandler; 44 | onEnterPress?: (e: React.KeyboardEvent) => void; 45 | onChange?: (val: string) => void; 46 | onClear?: () => void; 47 | onFocus?: (e: React.FocusEvent) => void; 48 | onBlur?: (e: React.FocusEvent) => void; 49 | } 50 | 51 | const classPrefix = `ygm-input`; 52 | 53 | const Input = React.forwardRef((props, ref) => { 54 | const [value, setValue] = React.useState(props.value!); 55 | const nativeInputRef = React.useRef(null); 56 | 57 | React.useImperativeHandle(ref, () => ({ 58 | clear: () => { 59 | setValue(''); 60 | }, 61 | focus: () => { 62 | nativeInputRef.current?.focus(); 63 | }, 64 | blur: () => { 65 | nativeInputRef.current?.blur(); 66 | }, 67 | setValue: (val: string) => { 68 | setValue(val); 69 | }, 70 | })); 71 | 72 | const handleKeydown = React.useCallback( 73 | (e: React.KeyboardEvent) => { 74 | if (props.onEnterPress && e.code === 'Enter') { 75 | props.onEnterPress(e); 76 | } 77 | props.onKeyDown?.(e); 78 | }, 79 | [props] 80 | ); 81 | 82 | const showClearable = React.useMemo(() => { 83 | if (!props.clearable || !value || props.readOnly) return false; 84 | return true; 85 | }, [props.clearable, props.readOnly, value]); 86 | 87 | return ( 88 |
89 | { 113 | setValue(e.target.value); 114 | props.onChange?.(e.target.value); 115 | }} 116 | onFocus={props.onFocus} 117 | onBlur={props.onBlur} 118 | /> 119 | 120 | {showClearable && ( 121 |
{ 124 | e.preventDefault(); 125 | }} 126 | onClick={() => { 127 | setValue(''); 128 | props.onClear?.(); 129 | }} 130 | > 131 | 132 |
133 | )} 134 |
135 | ); 136 | }); 137 | 138 | Input.defaultProps = { 139 | autoComplete: 'off', 140 | autoCapitalize: 'off', 141 | autoCorrect: 'off', 142 | value: '', 143 | id: 'ygm-input', 144 | type: 'text', 145 | }; 146 | 147 | Input.displayName = 'Input'; 148 | 149 | export default Input; 150 | -------------------------------------------------------------------------------- /packages/input/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-input: 'ygm-input'; 2 | 3 | .#{$class-prefix-input} { 4 | --color: var(--ygm-color-text); 5 | --placeholder-color: var(--ygm-color-light); 6 | 7 | --text-align: left; 8 | --background-color: transparent; 9 | --font-size: var(--ygm-font-size-m); 10 | 11 | display: flex; 12 | justify-content: flex-start; 13 | align-items: center; 14 | 15 | width: 100%; 16 | min-height: 24px; 17 | background-color: var(--background-color); 18 | 19 | &-disabled { 20 | opacity: 0.4; 21 | } 22 | 23 | &-element { 24 | flex: auto; 25 | display: inline-block; 26 | box-sizing: border-box; 27 | width: 100%; 28 | max-width: 100%; 29 | max-height: 100%; 30 | padding: 0; 31 | margin: 0; 32 | color: var(--color); 33 | font-size: var(--font-size); 34 | line-height: 1.5; 35 | background: transparent; 36 | border: 0; 37 | outline: none; 38 | appearance: none; 39 | text-align: var(--text-align); 40 | 41 | &::placeholder { 42 | color: var(--placeholder-color); 43 | font-family: inherit; 44 | } 45 | 46 | &:-webkit-autofill { 47 | background-color: transparent; 48 | } 49 | 50 | &:read-only { 51 | cursor: default; 52 | } 53 | 54 | &:invalid { 55 | box-shadow: none; 56 | } 57 | 58 | &::-ms-clear { 59 | display: none; 60 | } 61 | &::-webkit-search-cancel-button { 62 | display: none; 63 | } 64 | &::-webkit-search-decoration { 65 | display: none; 66 | } 67 | &:disabled { 68 | opacity: 1; 69 | } 70 | 71 | // for ios 72 | &[type='date'], 73 | &[type='time'], 74 | &[type='datetime-local'] { 75 | min-height: 1.5em; 76 | } 77 | 78 | // for safari 79 | &[type='search'] { 80 | -webkit-appearance: none; 81 | } 82 | 83 | &[readonly] { 84 | pointer-events: none; 85 | } 86 | } 87 | 88 | &-clear { 89 | flex: none; 90 | margin-left: 8px; 91 | color: var(--ygm-color-light); 92 | &:active { 93 | color: var(--ygm-color-weak); 94 | } 95 | padding: 4px; 96 | cursor: pointer; 97 | .antd-mobile-icon { 98 | display: block; 99 | font-size: var(--ygm-font-size-l); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /packages/mask/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSpring, animated } from '@react-spring/web'; 3 | 4 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect'; 5 | import useScrollLock from '@/hooks/useScrollLock'; 6 | 7 | import './styles/index.scss'; 8 | 9 | export interface MaskProps { 10 | /** 是否可见 */ 11 | visible: boolean; 12 | /** 点击蒙层触发回调 */ 13 | onMaskClick?: (event: React.MouseEvent) => void; 14 | style?: React.CSSProperties & Partial>; 15 | } 16 | 17 | const classPrefix = 'ygm-mask'; 18 | 19 | const Mask: React.FC = (props) => { 20 | const [active, setActive] = React.useState(props.visible); 21 | 22 | useScrollLock(props.visible); 23 | 24 | const onMask = React.useCallback( 25 | (e: React.MouseEvent) => { 26 | e.stopPropagation(); 27 | props.onMaskClick?.(e); 28 | }, 29 | [props.onMaskClick] 30 | ); 31 | 32 | const { opacity } = useSpring({ 33 | opacity: props.visible ? 1 : 0, 34 | config: { 35 | tension: 250, 36 | friction: 30, 37 | clamp: true, 38 | }, 39 | onRest: () => { 40 | setActive(props.visible); 41 | }, 42 | }); 43 | 44 | useIsomorphicLayoutEffect(() => { 45 | if (props.visible) { 46 | setActive(true); 47 | } 48 | }, [props.visible]); 49 | 50 | return ( 51 | 56 | ); 57 | }; 58 | 59 | export default Mask; 60 | 61 | Mask.displayName = 'Mask'; 62 | -------------------------------------------------------------------------------- /packages/mask/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-mask: 'ygm-mask'; 2 | 3 | .#{$class-prefix-mask} { 4 | --z-index: 999; 5 | --background: rgba(0, 0, 0, 0.55); 6 | 7 | position: fixed; 8 | top: 0; 9 | left: 0; 10 | z-index: var(--z-index); 11 | width: 100%; 12 | height: 100%; 13 | display: block; 14 | background: var(--background); 15 | overflow: hidden; 16 | } 17 | -------------------------------------------------------------------------------- /packages/nav-bar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { LeftOutline } from 'antd-mobile-icons'; 4 | 5 | import './styles/index.scss'; 6 | 7 | export interface NavBarProps { 8 | /** 点击返回区域后的回调 */ 9 | onBack?: () => void; 10 | /** 右侧内容 */ 11 | right?: React.ReactNode; 12 | /** 中间内容 */ 13 | children?: React.ReactNode; 14 | /** 是否显示返回区域的箭头 */ 15 | leftArrow?: boolean; 16 | /** 返回区域文字 */ 17 | leftText?: string; 18 | /** 样式 */ 19 | style?: React.CSSProperties & Partial>; 20 | } 21 | 22 | const classPrefix = 'ygm-nav-bar'; 23 | 24 | const NavBar: React.FC = (props) => { 25 | return ( 26 |
27 |
28 | {props.leftArrow && ( 29 |
30 | 31 |
32 | )} 33 |
{props.leftText}
34 |
35 |
{props.children}
36 |
{props.right}
37 |
38 | ); 39 | }; 40 | 41 | NavBar.defaultProps = { 42 | leftText: '', 43 | leftArrow: true, 44 | }; 45 | 46 | NavBar.displayName = 'NavBar'; 47 | 48 | export default NavBar; 49 | -------------------------------------------------------------------------------- /packages/nav-bar/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-nav-bar: 'ygm-nav-bar'; 2 | 3 | .#{$class-prefix-nav-bar} { 4 | --nav-bar-height: 45px; 5 | --border-bottom: none; 6 | 7 | height: var(--nav-bar-height); 8 | border-bottom: var(--border-bottom); 9 | padding: 0 var(--ygm-padding-l); 10 | white-space: nowrap; 11 | display: flex; 12 | align-items: center; 13 | background-color: var(--ygm-color-background); 14 | box-sizing: border-box; 15 | 16 | &-left, 17 | &-right { 18 | flex: 1; 19 | } 20 | 21 | &-title { 22 | flex: auto; 23 | text-align: center; 24 | overflow: hidden; 25 | text-overflow: ellipsis; 26 | font-size: var(--ygm-font-size-l); 27 | } 28 | 29 | &-left { 30 | font-size: var(--ygm-font-size-m); 31 | display: flex; 32 | justify-content: flex-start; 33 | align-items: center; 34 | 35 | &-icon { 36 | font-size: var(--ygm-font-size-xl); 37 | margin-right: 4px; 38 | } 39 | } 40 | 41 | &-right { 42 | text-align: right; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/popup/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | // 它基于弹簧物理原理实现,他的核心理念就是 5 | // 使我们元素的动画轨迹和真实世界更接近 6 | import { useSpring, animated } from '@react-spring/web'; 7 | 8 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect'; 9 | import useScrollLock from '@/hooks/useScrollLock'; 10 | 11 | import Mask from '@/mask'; 12 | 13 | import './styles/index.scss'; 14 | 15 | export interface PopupProps { 16 | /** 指定弹出的位置 */ 17 | position?: 'left' | 'top' | 'bottom' | 'right'; 18 | /** 内容区域style属性 */ 19 | style?: React.CSSProperties; 20 | /** 内容区域类名 */ 21 | className?: string; 22 | /** 是否可见 */ 23 | visible: boolean; 24 | children?: React.ReactNode; 25 | /** 是否展示蒙层 */ 26 | mask?: boolean; 27 | /** 点击蒙层回调 */ 28 | onMaskClick?: (event: React.MouseEvent) => void; 29 | /** 显示后回调 */ 30 | afterShow?: () => void; 31 | /** 关闭后回调 */ 32 | afterClose?: () => void; 33 | } 34 | 35 | const classPrefix = 'ygm-popup'; 36 | 37 | const Popup: React.FC = (props) => { 38 | const [active, setActive] = React.useState(props.visible); 39 | 40 | useScrollLock(props.visible); 41 | 42 | const { percent } = useSpring({ 43 | percent: props.visible ? 0 : 100, 44 | config: { 45 | // 精确度 46 | precision: 0.1, 47 | // 弹簧质量,mass的值越大,动画执行的速度也会随着执行的时间变得越变越快 48 | mass: 0.4, 49 | // 弹簧张力 50 | tension: 300, 51 | // 表示摩擦力和阻力 52 | friction: 30, 53 | }, 54 | onRest: () => { 55 | setActive(props.visible); 56 | if (props.visible) { 57 | props.afterClose?.(); 58 | } else { 59 | props.afterClose?.(); 60 | } 61 | }, 62 | }); 63 | 64 | useIsomorphicLayoutEffect(() => { 65 | if (props.visible) { 66 | setActive(true); 67 | } 68 | }, [props.visible]); 69 | 70 | return ( 71 |
72 | {props.mask && } 73 | 74 | { 79 | if (props.position === 'bottom') { 80 | return `translate(0, ${v}%)`; 81 | } 82 | if (props.position === 'left') { 83 | return `translate(-${v}%, 0)`; 84 | } 85 | if (props.position === 'right') { 86 | return `translate(${v}%, 0)`; 87 | } 88 | if (props.position === 'top') { 89 | return `translate(0, -${v}%)`; 90 | } 91 | return 'none'; 92 | }), 93 | }} 94 | > 95 | {props.children} 96 | 97 |
98 | ); 99 | }; 100 | 101 | Popup.defaultProps = { 102 | visible: false, 103 | position: 'left', 104 | mask: true, 105 | }; 106 | 107 | export default Popup; 108 | 109 | Popup.displayName = 'Popup'; 110 | -------------------------------------------------------------------------------- /packages/popup/styles/index.scss: -------------------------------------------------------------------------------- 1 | .ygm-popup { 2 | --z-index: 1000; 3 | 4 | z-index: var(--z-index); 5 | position: fixed; 6 | background-color: var(--ygm-color-background); 7 | 8 | &-body { 9 | position: fixed; 10 | background-color: var(--ygm-color-white); 11 | z-index: calc(var(--z-index) + 10); 12 | } 13 | 14 | &-left { 15 | height: 100%; 16 | top: 0; 17 | left: 0; 18 | } 19 | 20 | &-bottom { 21 | width: 100%; 22 | bottom: 0; 23 | left: 0; 24 | } 25 | &-top { 26 | width: 100%; 27 | top: 0; 28 | left: 0; 29 | } 30 | 31 | &-right { 32 | height: 100%; 33 | top: 0; 34 | right: 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/pull-to-refresh/constants.ts: -------------------------------------------------------------------------------- 1 | import { TPullStatus, TPullKey } from './types'; 2 | 3 | export const PULL_STATUS: Record = { 4 | PULLING: 'pulling', 5 | CAN_RELEASE: 'canRelease', 6 | REFRESHING: 'refreshing', 7 | COMPLETE: 'complete', 8 | }; 9 | 10 | export const DEFUALT_DURATION = 300; 11 | 12 | export const FRICTION = 0.3; 13 | -------------------------------------------------------------------------------- /packages/pull-to-refresh/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SpinnerLoading from '@/spinner-loading'; 3 | 4 | import { getScrollParent, getScrollTop, sleep } from './utils'; 5 | import { TPullStatus } from './types'; 6 | import { PULL_STATUS, DEFUALT_DURATION, FRICTION } from './constants'; 7 | 8 | import './styles/index.scss'; 9 | 10 | export interface PullToRefreshProps { 11 | children: React.ReactNode; 12 | pullingText?: React.ReactNode; 13 | canReleaseText?: React.ReactNode; 14 | refreshingText?: React.ReactNode; 15 | completeText?: React.ReactNode; 16 | headHeight?: number; 17 | threshold?: number; 18 | completeDelay?: number; 19 | onRefresh: () => Promise; 20 | } 21 | 22 | const PullToRefresh: React.FC = React.memo((props) => { 23 | const [status, setStatus] = React.useState(PULL_STATUS.PULLING); 24 | const [pullDistance, setPullDistance] = React.useState(0); 25 | const [duration, setDuration] = React.useState(DEFUALT_DURATION); 26 | 27 | const containerRef = React.useRef(null); 28 | const touchStartY = React.useRef(0); 29 | const isDragging = React.useRef(false); 30 | 31 | const trackStyle = React.useMemo(() => { 32 | return { 33 | transitionDuration: `${duration}ms`, 34 | transform: `translate3d(0,${pullDistance}px,0)`, 35 | }; 36 | }, [duration, pullDistance]); 37 | 38 | const isTouchable = React.useMemo(() => { 39 | return status !== PULL_STATUS.REFRESHING && status !== PULL_STATUS.COMPLETE; 40 | }, [status]); 41 | 42 | const renderStatusText = React.useCallback(() => { 43 | if (status === PULL_STATUS.PULLING) return props.pullingText; 44 | else if (status === PULL_STATUS.CAN_RELEASE) return props.canReleaseText; 45 | else if (status === PULL_STATUS.REFRESHING) return props.refreshingText; 46 | return props.completeText; 47 | }, [props.canReleaseText, props.completeText, props.pullingText, props.refreshingText, status]); 48 | 49 | const onTouchEnd = React.useCallback(async () => { 50 | if (!isDragging.current && !isTouchable) return; 51 | isDragging.current = false; 52 | setDuration(DEFUALT_DURATION); 53 | if (status === PULL_STATUS.CAN_RELEASE) { 54 | setStatus(PULL_STATUS.REFRESHING); 55 | setPullDistance(props.headHeight!); 56 | try { 57 | await props.onRefresh(); 58 | setStatus(PULL_STATUS.COMPLETE); 59 | } catch (e) { 60 | setPullDistance(0); 61 | setStatus(PULL_STATUS.PULLING); 62 | throw e; 63 | } 64 | 65 | if (props.completeDelay! > 0) { 66 | await sleep(props.completeDelay!); 67 | } 68 | } 69 | setPullDistance(0); 70 | setStatus(PULL_STATUS.PULLING); 71 | }, [isTouchable, status, props]); 72 | 73 | const onTouchMove = React.useCallback( 74 | (e: TouchEvent) => { 75 | if (!isDragging.current && !isTouchable) return; 76 | const currentY = e.changedTouches[0].clientY; 77 | const diff = (currentY - touchStartY.current) * FRICTION; 78 | 79 | if (diff <= 0) return; 80 | 81 | if (diff > props.threshold!) { 82 | setStatus(PULL_STATUS.CAN_RELEASE); 83 | } else { 84 | setStatus(PULL_STATUS.PULLING); 85 | } 86 | setPullDistance(diff); 87 | }, 88 | [isTouchable, props.threshold] 89 | ); 90 | 91 | const onTouchStart = React.useCallback( 92 | (e: TouchEvent) => { 93 | if (!isTouchable) return; 94 | const scrollParent = getScrollParent(e.target as Element); 95 | 96 | const scrollTop = getScrollTop(scrollParent as Element); 97 | if (scrollTop === 0) { 98 | setDuration(0); 99 | touchStartY.current = e.changedTouches[0].clientY; 100 | isDragging.current = true; 101 | } 102 | }, 103 | [isTouchable] 104 | ); 105 | 106 | React.useEffect(() => { 107 | const element = containerRef.current; 108 | if (!element) return; 109 | 110 | element.addEventListener('touchstart', onTouchStart); 111 | element.addEventListener('touchmove', onTouchMove); 112 | element.addEventListener('touchend', onTouchEnd); 113 | 114 | return () => { 115 | element.removeEventListener('touchstart', onTouchStart); 116 | element.removeEventListener('touchmove', onTouchMove); 117 | element.removeEventListener('touchend', onTouchEnd); 118 | }; 119 | }, [onTouchEnd, onTouchMove, onTouchStart]); 120 | 121 | return ( 122 |
123 |
124 |
125 | {renderStatusText()} 126 |
127 |
{props.children}
128 |
129 |
130 | ); 131 | }); 132 | 133 | PullToRefresh.defaultProps = { 134 | pullingText: '下拉刷新', 135 | canReleaseText: '释放立即刷新', 136 | refreshingText: , 137 | completeText: '刷新成功', 138 | headHeight: 30, 139 | threshold: 50, 140 | completeDelay: 500, 141 | }; 142 | 143 | PullToRefresh.displayName = 'PullToRefresh'; 144 | 145 | export default PullToRefresh; 146 | -------------------------------------------------------------------------------- /packages/pull-to-refresh/styles/index.scss: -------------------------------------------------------------------------------- 1 | .ygm-pull-to-refresh { 2 | overflow: hidden; 3 | user-select: none; 4 | &-head { 5 | position: relative; 6 | height: 100%; 7 | transition-property: transform; 8 | 9 | &-content { 10 | font-size: var(--ygm-font-size-s); 11 | overflow: hidden; 12 | position: absolute; 13 | left: 0; 14 | width: 100%; 15 | color: var(--ygm-color-weak); 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | transform: translateY(-100%); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/pull-to-refresh/types.ts: -------------------------------------------------------------------------------- 1 | export type TPullStatus = 'pulling' | 'canRelease' | 'refreshing' | 'complete'; 2 | export type TPullKey = 'PULLING' | 'CAN_RELEASE' | 'REFRESHING' | 'COMPLETE'; 3 | -------------------------------------------------------------------------------- /packages/pull-to-refresh/utils.ts: -------------------------------------------------------------------------------- 1 | const inBrowser = typeof window !== 'undefined'; 2 | 3 | const defaultRoot = inBrowser ? window : undefined; 4 | 5 | type ScrollElement = HTMLElement | Window; 6 | 7 | const overflowStylePatterns = ['scroll', 'auto']; 8 | 9 | const isElement = (node: Element) => { 10 | const ELEMENT_NODE_TYPE = 1; 11 | return node.tagName !== 'HTML' && node.tagName !== 'BODY' && node.nodeType === ELEMENT_NODE_TYPE; 12 | }; 13 | 14 | export const getScrollParent = (el: Element, root: ScrollElement | undefined = defaultRoot) => { 15 | let node = el; 16 | 17 | while (node && node !== root && isElement(node)) { 18 | const { overflowY } = window.getComputedStyle(node); 19 | if (overflowStylePatterns.includes(overflowY) && node.scrollHeight > node.clientHeight) { 20 | return node; 21 | } 22 | node = node.parentNode as Element; 23 | } 24 | 25 | return root; 26 | }; 27 | 28 | export const getScrollTop = (element: Window | Element) => { 29 | const top = 'scrollTop' in element ? element.scrollTop : element.scrollY; 30 | 31 | // iOS scroll bounce cause minus scrollTop 32 | return Math.max(top, 0); 33 | }; 34 | 35 | export const sleep = (time: number) => 36 | new Promise((resolve) => { 37 | window.setTimeout(resolve, time); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/search-bar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Input, { InputRef } from '@/input'; 3 | 4 | import { SearchOutline } from 'antd-mobile-icons'; 5 | 6 | import './styles/index.scss'; 7 | 8 | const classPrefix = `ygm-search-bar`; 9 | 10 | type TStyle = Partial< 11 | Record<'--color' | '--background' | '--search-background' | '--border-radius' | '--placeholder-color', string> 12 | >; 13 | 14 | export type SearchBarRef = InputRef; 15 | 16 | export interface SearchBarProps { 17 | /** 输入内容 */ 18 | value?: string; 19 | /** 提示文本 */ 20 | placeholder?: string; 21 | /** 搜索框前缀图标 */ 22 | icon?: React.ReactNode; 23 | /** 输入的最大字符数 */ 24 | maxLength?: number; 25 | /** 是否显示清除图标,可点击清除文本框 */ 26 | clearable?: boolean; 27 | /** 禁止输入 */ 28 | disabled?: boolean; 29 | style?: React.CSSProperties & TStyle; 30 | /** 取消按钮文案 */ 31 | cancelText?: string; 32 | /** 是否显示取消按钮 */ 33 | showCancel?: boolean; 34 | /** 点击取消按钮时触发事件 */ 35 | onCancel?: () => void; 36 | /** 输入框回车键触发事件 */ 37 | onSearch?: (val: string) => void; 38 | /** 输入框内容变化时触发事件 */ 39 | onChange?: (val: string) => void; 40 | /** 点击清除图标时触发事件 */ 41 | onClear?: () => void; 42 | } 43 | 44 | const SearchBar = React.forwardRef((props, ref) => { 45 | const [value, setValue] = React.useState(props.value!); 46 | const composingRef = React.useRef(false); 47 | const inputRef = React.useRef(null); 48 | 49 | React.useImperativeHandle(ref, () => ({ 50 | clear: () => inputRef.current?.clear(), 51 | focus: () => inputRef.current?.focus(), 52 | blur: () => inputRef.current?.blur(), 53 | setValue: (val: string) => inputRef.current?.setValue(val), 54 | })); 55 | 56 | const onChange = (value: string) => { 57 | setValue(value); 58 | props.onChange?.(value); 59 | }; 60 | 61 | const onEnterPress = () => { 62 | // 在拼音输入法输入汉字时,避免enter键的搜索触发 63 | if (!composingRef.current) { 64 | inputRef.current?.blur(); 65 | props.onSearch?.(value); 66 | } 67 | }; 68 | 69 | return ( 70 |
71 |
72 |
{props.icon}
73 | { 88 | composingRef.current = true; 89 | }} 90 | onCompositionEnd={() => { 91 | composingRef.current = false; 92 | }} 93 | /> 94 |
95 | {props.showCancel && ( 96 |
97 | {props.cancelText} 98 |
99 | )} 100 |
101 | ); 102 | }); 103 | 104 | SearchBar.defaultProps = { 105 | value: '', 106 | icon: , 107 | clearable: true, 108 | cancelText: '取消', 109 | }; 110 | 111 | SearchBar.displayName = 'SearchBar'; 112 | 113 | export default SearchBar; 114 | -------------------------------------------------------------------------------- /packages/search-bar/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-search-bar: 'ygm-search-bar'; 2 | 3 | .#{$class-prefix-search-bar} { 4 | --height: 32px; 5 | --padding-left: var(--ygm-padding-s); 6 | --background: var(--ygm-color-background); 7 | --search-background: var(--ygm-color-box); 8 | --border-radius: var(--ygm-radius-xs); 9 | 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | box-sizing: border-box; 14 | padding: 10px 16px; 15 | background-color: var(--background); 16 | 17 | &-content { 18 | align-items: center; 19 | justify-content: center; 20 | display: flex; 21 | flex: 1; 22 | padding: 5px 12px 5px; 23 | background-color: var(--search-background); 24 | border-radius: var(--border-radius); 25 | 26 | &-icon { 27 | flex: none; 28 | color: var(--ygm-color-weak); 29 | font-size: var(--ygm-font-size-l); 30 | margin-right: 8px; 31 | } 32 | 33 | &-input { 34 | flex: 1; 35 | } 36 | 37 | &-cancel { 38 | padding-left: var(--ygm-padding-s); 39 | color: var(--ygm-color-text); 40 | font-size: var(--ygm-font-size-m); 41 | line-height: 34px; 42 | cursor: pointer; 43 | user-select: none; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/selector/CheckMark.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CheckMark = React.memo(() => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | }); 20 | 21 | export default CheckMark; 22 | -------------------------------------------------------------------------------- /packages/selector/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import Space from '@/space'; 5 | import Grid from '@/grid'; 6 | 7 | import CheckMark from '@/selector/CheckMark'; 8 | 9 | import './styles/index.scss'; 10 | 11 | export interface SelectorOption { 12 | label: React.ReactNode; 13 | value: V; 14 | description?: React.ReactNode; 15 | disabled?: boolean; 16 | } 17 | 18 | export interface SelectorProps { 19 | options: SelectorOption[]; 20 | value?: V[]; 21 | /** 布局列数 */ 22 | columns?: number; 23 | /** label间距 */ 24 | gap?: number | string | [number | string, number | string]; 25 | /** 是否支持多选 */ 26 | multiple?: boolean; 27 | showCheckMark?: boolean; 28 | onChange?: (v: V[], items: SelectorOption[]) => void; 29 | /** 自定义样式 */ 30 | style?: React.CSSProperties & 31 | Partial< 32 | Record< 33 | | '--color' 34 | | '--checked-color' 35 | | '--text-color' 36 | | '--checked-text-color' 37 | | '--border' 38 | | '--checked-border' 39 | | '--border-radius' 40 | | '--padding', 41 | string 42 | > 43 | >; 44 | } 45 | 46 | type SelectorValue = string | number; 47 | 48 | const classPrefix = `ygm-selector`; 49 | 50 | const Selector = (props: SelectorProps) => { 51 | const [value, setValue] = React.useState(props.value!); 52 | 53 | const items = props.options.map((option) => { 54 | const active = value.includes(option.value); 55 | const disabled = !!option.disabled; 56 | const className = cx(`${classPrefix}-item`, { 57 | [`${classPrefix}-item-active`]: active, 58 | [`${classPrefix}-item-disabled`]: disabled, 59 | }); 60 | 61 | const onClick = () => { 62 | if (disabled) return; 63 | if (props.multiple) { 64 | const val = active ? value.filter((v) => v !== option.value) : [...value, option.value]; 65 | const selectorOptions = props.options.filter((option) => val.includes(option.value)); 66 | setValue(val); 67 | props.onChange?.(val, selectorOptions); 68 | } else { 69 | const val = active ? [] : [option.value]; 70 | setValue(val); 71 | props.onChange?.(val, [option]); 72 | } 73 | }; 74 | 75 | return ( 76 |
77 | {option.label} 78 | {option.description &&
{option.description}
} 79 | 80 | {active && props.showCheckMark && ( 81 |
82 | 83 |
84 | )} 85 |
86 | ); 87 | }); 88 | 89 | return ( 90 |
91 | {!props.columns ? ( 92 | 93 | {items} 94 | 95 | ) : ( 96 | 97 | {items} 98 | 99 | )} 100 |
101 | ); 102 | }; 103 | 104 | Selector.displayName = 'Selector'; 105 | 106 | Selector.defaultProps = { 107 | value: [], 108 | gap: 8, 109 | showCheckMark: true, 110 | }; 111 | export default Selector; 112 | -------------------------------------------------------------------------------- /packages/selector/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-selector: 'ygm-selector'; 2 | 3 | .#{$class-prefix-selector} { 4 | --color: #f5f5f5; 5 | --checked-color: #e7f1ff; 6 | --text-color: var(--ygm-color-text); 7 | --checked-text-color: var(--ygm-color-primary); 8 | --padding: 8px 16px; 9 | --border-radius: 2px; 10 | --border: none; 11 | --checked-border: none; 12 | 13 | overflow: hidden; 14 | font-size: var(--ygm-font-size-m); 15 | user-select: none; 16 | line-height: 1.4; 17 | 18 | &-item { 19 | position: relative; 20 | padding: var(--padding); 21 | background-color: var(--color); 22 | border: var(--border); 23 | border-radius: var(--border-radius); 24 | color: var(--text-color); 25 | display: inline-block; 26 | text-align: center; 27 | overflow: hidden; 28 | vertical-align: top; 29 | cursor: pointer; 30 | opacity: 1; 31 | 32 | &-active { 33 | color: var(--checked-text-color); 34 | background-color: var(--checked-color); 35 | border: var(--checked-border); 36 | } 37 | 38 | &-disabled { 39 | cursor: not-allowed; 40 | opacity: 0.4; 41 | } 42 | 43 | &-description { 44 | font-size: var(--ygm-font-size-s); 45 | color: var(--ygm-color-weak); 46 | } 47 | 48 | .#{$class-prefix-selector}-check-mark { 49 | position: absolute; 50 | right: 0; 51 | bottom: 0; 52 | width: 0; 53 | height: 0; 54 | border-top: solid 8px transparent; 55 | border-bottom: solid 8px var(--ygm-color-primary); 56 | border-left: solid 10px transparent; 57 | border-right: solid 10px var(--ygm-color-primary); 58 | 59 | > svg { 60 | position: absolute; 61 | left: 0; 62 | top: 0; 63 | height: 6px; 64 | width: 8px; 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import InternalSidebar from '@/sidebar/sidebar'; 2 | import SidebarItem from '@/sidebar/sidebar-item'; 3 | 4 | export type { SidebarProps } from '@/sidebar/sidebar'; 5 | export type { SidebarItemProps } from '@/sidebar/sidebar-item'; 6 | 7 | type InternalSidebarType = typeof InternalSidebar; 8 | 9 | export interface SidebarInterface extends InternalSidebarType { 10 | Item: typeof SidebarItem; 11 | } 12 | 13 | const Sidebar = InternalSidebar as SidebarInterface; 14 | 15 | Sidebar.Item = SidebarItem; 16 | 17 | export default Sidebar; 18 | -------------------------------------------------------------------------------- /packages/sidebar/sidebar-item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface SidebarItemProps { 4 | key: string; 5 | title: React.ReactNode; 6 | children: React.ReactNode; 7 | } 8 | 9 | const SidebarItem: React.FC = (props) => { 10 | return props.children ? (props.children as React.ReactElement) : null; 11 | }; 12 | 13 | SidebarItem.displayName = 'SidebarItem'; 14 | 15 | export default SidebarItem; 16 | -------------------------------------------------------------------------------- /packages/sidebar/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import SidebarItem from '@/sidebar/sidebar-item'; 5 | import { traverseReactNode } from '@/utils/traverse-react-node'; 6 | 7 | import './styles/index.scss'; 8 | 9 | export interface SidebarProps { 10 | /** 当前激活side item面板的key */ 11 | activeKey: string; 12 | /** 点击side item切换后回调 */ 13 | onChange?: (key: string) => void; 14 | children?: React.ReactNode; 15 | /** 基本样式 */ 16 | style?: React.CSSProperties & 17 | Partial< 18 | Record<'--width' | '--height' | '--background-color' | '--content-padding' | '--sidebar-item-padding', string> 19 | >; 20 | } 21 | 22 | const classPrefix = `ygm-sidebar`; 23 | 24 | const Sidebar: React.FC = React.memo((props) => { 25 | const [activeKey, setActiveKey] = React.useState(props.activeKey); 26 | 27 | const items: React.ReactElement>[] = []; 28 | 29 | traverseReactNode(props.children, (child) => { 30 | if (!React.isValidElement(child)) return; 31 | if (!child.key) return; 32 | items.push(child); 33 | }); 34 | 35 | const onSetActive = (e: React.MouseEvent) => { 36 | const key = (e.target as HTMLElement).dataset['key']; 37 | setActiveKey(key as string); 38 | props.onChange?.(key as string); 39 | }; 40 | 41 | return ( 42 |
43 |
44 | {items.map((item) => { 45 | const active = item.key === activeKey; 46 | return ( 47 |
55 |
56 | {item.props.title} 57 |
58 |
59 | ); 60 | })} 61 |
62 | 63 |
64 | {items.map((item) => ( 65 |
70 | {item.props.children} 71 |
72 | ))} 73 |
74 |
75 | ); 76 | }); 77 | 78 | Sidebar.displayName = 'Sidebar'; 79 | 80 | export default Sidebar; 81 | -------------------------------------------------------------------------------- /packages/sidebar/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-sidebar: 'ygm-sidebar'; 2 | 3 | .#{$class-prefix-sidebar} { 4 | --height: 100%; 5 | --width: 80px; 6 | --content-padding: 0; 7 | --sidebar-item-padding: 16px 5px; 8 | --item-border-radius: var(--ygm-radius-m); 9 | --background-color: var(--ygm-color-box); 10 | 11 | height: var(--height); 12 | font-size: var(--ygm-font-size-m); 13 | display: flex; 14 | 15 | &-items { 16 | width: var(--width); 17 | box-sizing: border-box; 18 | overflow-y: auto; 19 | background-color: var(--background-color); 20 | } 21 | 22 | &-content { 23 | flex: 1; 24 | overflow: hidden; 25 | overflow-y: auto; 26 | background-color: var(--ygm-color-white); 27 | padding: var(--content-padding); 28 | } 29 | 30 | &-item { 31 | width: 100%; 32 | box-sizing: border-box; 33 | padding: var(--sidebar-item-padding); 34 | text-align: center; 35 | position: relative; 36 | cursor: pointer; 37 | 38 | &-active { 39 | background-color: var(--ygm-color-white); 40 | position: relative; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/slider/index.tsx: -------------------------------------------------------------------------------- 1 | import Slider from '@/slider/slider'; 2 | 3 | export type { SliderProps, SliderRef } from '@/slider/slider'; 4 | 5 | export default Slider; 6 | -------------------------------------------------------------------------------- /packages/slider/slider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import Thumb from '@/slider/thumb'; 5 | 6 | import { getValueByScope } from '@/utils/utils'; 7 | 8 | import './styles/slider.scss'; 9 | 10 | export interface SliderRef { 11 | setValue: (value: number) => void; 12 | } 13 | 14 | export interface SliderProps { 15 | min?: number; 16 | max?: number; 17 | value?: number; 18 | step?: number; 19 | disabled?: boolean; 20 | onChange?: (value: number) => void; 21 | onChangeAfter?: (value: number) => void; 22 | style?: React.CSSProperties & 23 | Partial>; 24 | } 25 | 26 | const classPrefix = 'ygm-slider'; 27 | 28 | const Slider = React.forwardRef((props, ref) => { 29 | const [sliderValue, setSliderValue] = React.useState(getValueByScope(props.value!, props.min!, props.max!)); 30 | 31 | const trackRef = React.useRef(null); 32 | 33 | React.useImperativeHandle(ref, () => ({ 34 | setValue: (val: number) => { 35 | setSliderValue(getValueByScope(val, props.min!, props.max!)); 36 | }, 37 | })); 38 | 39 | // 滚动条值范围 40 | const scope = props.max! - props.min!; 41 | // 计算滚动的百分比 42 | const fillSize = `${((sliderValue - props.min!) * 100) / scope}%`; 43 | 44 | const getValueByPosition = (position: number) => { 45 | const newPosition = getValueByScope(position, props.min!, props.max!); 46 | // 除以step得到可以移动的步数取整,再乘以步数得到真实的value值 47 | const value = Math.round(newPosition / props.step!) * props.step!; 48 | 49 | return value; 50 | }; 51 | 52 | const onTrack = (e: React.MouseEvent) => { 53 | e.stopPropagation(); 54 | const track = trackRef.current; 55 | if (props.disabled || !track) return; 56 | 57 | const rect = track.getBoundingClientRect(); 58 | // 滚动条总长度 59 | const sliderWidth = rect.width; 60 | // 滚动条跟视口的距离 61 | const sliderOffsetLeft = rect.left; 62 | // 滚动距离 63 | const delta = e.clientX - sliderOffsetLeft; 64 | // 占总长度百分比 * 范围长度得到真实的position值 65 | const position = props.min! + (delta / sliderWidth) * scope; 66 | 67 | const targetValue = getValueByPosition(position); 68 | 69 | setSliderValue(targetValue); 70 | props.onChangeAfter?.(targetValue); 71 | }; 72 | 73 | const onDrag = (position: number) => { 74 | const targetValue = getValueByPosition(position); 75 | setSliderValue(targetValue); 76 | props.onChange?.(targetValue); 77 | }; 78 | 79 | const onEnd = (position: number) => { 80 | const targetValue = getValueByPosition(position); 81 | props.onChangeAfter?.(targetValue); 82 | }; 83 | 84 | return ( 85 |
91 |
97 | 106 |
107 | ); 108 | }); 109 | 110 | Slider.defaultProps = { 111 | min: 0, 112 | max: 100, 113 | step: 1, 114 | disabled: false, 115 | value: 0, 116 | }; 117 | 118 | Slider.displayName = 'Slider'; 119 | 120 | export default Slider; 121 | -------------------------------------------------------------------------------- /packages/slider/styles/slider.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-slider: 'ygm-slider'; 2 | 3 | .#{$class-prefix-slider} { 4 | --slider-bar-fill-color: var(--ygm-color-primary); 5 | --slider-bar-height: 2px; 6 | --slider-background-color: #ebedf0; 7 | --slider-border-radius: var(--ygm-radius-xs); 8 | 9 | position: relative; 10 | width: 100%; 11 | height: var(--slider-bar-height); 12 | background: var(--slider-background-color); 13 | border-radius: var(--slider-border-radius); 14 | 15 | &::before { 16 | position: absolute; 17 | top: calc(var(--ygm-padding-s) * -1); 18 | right: 0; 19 | bottom: calc(var(--ygm-padding-s) * -1); 20 | left: 0; 21 | content: ''; 22 | cursor: grab; 23 | } 24 | 25 | &-fill { 26 | position: absolute; 27 | left: 0; 28 | z-index: 1; 29 | height: var(--slider-bar-height); 30 | border-radius: var(--slider-border-radius); 31 | background-color: var(--slider-bar-fill-color); 32 | } 33 | 34 | &-disabled { 35 | opacity: 0.4; 36 | 37 | .#{$class-prefix-slider}-thumb { 38 | cursor: not-allowed; 39 | } 40 | &.#{$class-prefix-slider}::before { 41 | cursor: not-allowed; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/slider/styles/thumb.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-slider-thumb: 'ygm-slider-thumb'; 2 | 3 | .#{$class-prefix-slider-thumb} { 4 | touch-action: none; 5 | position: absolute; 6 | z-index: 2; 7 | border-radius: 50%; 8 | top: 50%; 9 | transform: translate(-50%, -50%); 10 | cursor: grab; 11 | 12 | &-button { 13 | width: 24px; 14 | height: 24px; 15 | background: var(--ygm-color-background); 16 | border-radius: 50%; 17 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/slider/thumb.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './styles/thumb.scss'; 4 | 5 | interface ThumbProps { 6 | value: number; 7 | min: number; 8 | max: number; 9 | disabled: boolean; 10 | trackRef: React.RefObject; 11 | onDrag: (value: number) => void; 12 | onChangeAfter: (value: number) => void; 13 | } 14 | 15 | const classPrefix = 'ygm-slider-thumb'; 16 | 17 | const Thumb: React.FC = (props) => { 18 | const prevValue = React.useRef(0); 19 | 20 | const startX = React.useRef(0); 21 | const endX = React.useRef(0); 22 | 23 | const currentPosition = `${((props.value - props.min) / (props.max - props.min)) * 100}%`; 24 | 25 | const onTouchStart = (e: React.TouchEvent) => { 26 | if (props.disabled) return; 27 | 28 | prevValue.current = props.value; 29 | startX.current = e.touches[0].clientX; 30 | }; 31 | 32 | const onTouchMove = (e: React.TouchEvent) => { 33 | const trackElement = props.trackRef.current; 34 | if (!trackElement || props.disabled) return; 35 | 36 | const deltaX = e.touches[0].clientX - startX.current; 37 | const total = trackElement.offsetWidth; 38 | 39 | // 移动距离:总长度 = 移动的实际距离 :实际距离 40 | const position = (deltaX / total) * (props.max - props.min); 41 | const finalPosition = position + prevValue.current; 42 | endX.current = finalPosition; 43 | props.onDrag(finalPosition); 44 | }; 45 | 46 | const onTouchEnd = () => { 47 | props?.onChangeAfter(endX.current); 48 | }; 49 | 50 | return ( 51 |
62 |
63 |
64 | ); 65 | }; 66 | 67 | Thumb.displayName = 'Thumb'; 68 | 69 | export default Thumb; 70 | -------------------------------------------------------------------------------- /packages/space/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import './styles/index.scss'; 5 | 6 | export interface SpaceProps { 7 | /** 间距方向 */ 8 | direction?: 'horizontal' | 'vertical'; 9 | /** 交叉轴对齐方式 */ 10 | align?: 'start' | 'end' | 'center' | 'baseline'; 11 | /** 主轴对齐方式 */ 12 | justify?: 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly' | 'stretch'; 13 | /** 是否自动换行,仅在 horizontal 时有效 */ 14 | wrap?: boolean; 15 | /** 是否渲染为块级元素 */ 16 | block?: boolean; 17 | /** 间距大小,设为数组时则分别设置垂直方向和水平方向的间距大小 */ 18 | gap?: number | string | [number | string, number | string]; 19 | /** 元素点击事件 */ 20 | onClick?: (event: React.MouseEvent) => void; 21 | children?: React.ReactNode; 22 | } 23 | 24 | const classPrefix = `ygm-space`; 25 | 26 | const formatGap = (gap: string | number) => (typeof gap === 'number' ? `${gap}px` : gap); 27 | 28 | const Space: React.FC = (props) => { 29 | const style = React.useMemo(() => { 30 | if (props.gap) { 31 | if (Array.isArray(props.gap)) { 32 | const [gapH, gapV] = props.gap; 33 | return { 34 | '--gap-vertical': formatGap(gapV), 35 | '--gap-horizontal': formatGap(gapH), 36 | }; 37 | } 38 | return { '--gap': formatGap(props.gap) }; 39 | } 40 | return {}; 41 | }, [props.gap]); 42 | 43 | return ( 44 |
55 | {React.Children.map(props.children, (child) => { 56 | return child !== null && child !== undefined &&
{child}
; 57 | })} 58 |
59 | ); 60 | }; 61 | 62 | Space.defaultProps = { 63 | direction: 'horizontal', 64 | block: true, 65 | }; 66 | 67 | Space.displayName = 'Space'; 68 | 69 | export default Space; 70 | -------------------------------------------------------------------------------- /packages/space/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-space: 'ygm-space'; 2 | 3 | .#{$class-prefix-space} { 4 | --gap: 8px; 5 | --gap-vertical: var(--gap); 6 | --gap-horizontal: var(--gap); 7 | 8 | display: inline-flex; 9 | 10 | &-horizontal { 11 | flex-direction: row; 12 | 13 | > .#{$class-prefix-space}-item { 14 | margin-right: var(--gap-horizontal); 15 | 16 | &:last-child { 17 | margin-right: 0; 18 | } 19 | } 20 | &.#{$class-prefix-space}-wrap { 21 | flex-wrap: wrap; 22 | margin-bottom: calc(var(--gap-vertical) * -1); 23 | > .#{$class-prefix-space}-item { 24 | padding-bottom: var(--gap-vertical); 25 | } 26 | } 27 | } 28 | 29 | &-vertical { 30 | flex-direction: column; 31 | 32 | > .#{$class-prefix-space}-item { 33 | margin-bottom: var(--gap-vertical); 34 | 35 | &:last-child { 36 | margin-bottom: 0; 37 | } 38 | } 39 | } 40 | 41 | &.#{$class-prefix-space}-block { 42 | display: flex; 43 | } 44 | 45 | &-align { 46 | &-center { 47 | align-items: center; 48 | } 49 | &-start { 50 | align-items: flex-start; 51 | } 52 | &-end { 53 | align-items: flex-end; 54 | } 55 | &-baseline { 56 | align-items: baseline; 57 | } 58 | } 59 | 60 | &-justify { 61 | &-center { 62 | justify-content: center; 63 | } 64 | &-start { 65 | justify-content: flex-start; 66 | } 67 | &-end { 68 | justify-content: flex-end; 69 | } 70 | &-between { 71 | justify-content: space-between; 72 | } 73 | &-around { 74 | justify-content: space-around; 75 | } 76 | &-evenly { 77 | justify-content: space-evenly; 78 | } 79 | &-stretch { 80 | justify-content: stretch; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/spinner-loading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import './styles/index.scss'; 5 | 6 | export interface SpinnerLoadingProps { 7 | type?: 'spinner'; 8 | color?: 'default' | 'primary' | 'white' | string; 9 | size?: number; 10 | } 11 | 12 | const colorRecord: Record = { 13 | default: true, 14 | primary: true, 15 | white: true, 16 | }; 17 | 18 | const SpinnerLoading: React.FC = React.memo((props) => { 19 | return ( 20 |
26 | ); 27 | }); 28 | 29 | SpinnerLoading.defaultProps = { 30 | color: 'default', 31 | size: 32, 32 | type: 'spinner', 33 | }; 34 | 35 | export default SpinnerLoading; 36 | 37 | SpinnerLoading.displayName = 'SpinnerLoading'; 38 | -------------------------------------------------------------------------------- /packages/spinner-loading/styles/index.scss: -------------------------------------------------------------------------------- 1 | .ygm-spinner { 2 | &-loading { 3 | border-top: 1px solid; 4 | border-right: 1px solid rgba(0, 0, 0, 0); 5 | border-bottom: 1px solid rgba(0, 0, 0, 0); 6 | border-left: 1px solid; 7 | border-radius: 50%; 8 | z-index: 1001; 9 | animation: spinner 0.8s infinite linear; 10 | } 11 | 12 | &-loading-color-default { 13 | border-top-color: var(--ygm-color-weak); 14 | border-left-color: var(--ygm-color-weak); 15 | } 16 | 17 | &-loading-color-primary { 18 | border-top-color: var(--ygm-color-primary); 19 | border-left-color: var(--ygm-color-primary); 20 | } 21 | 22 | &-loading-color-white { 23 | border-top-color: var(--ygm-color-white); 24 | border-left-color: var(--ygm-color-white); 25 | } 26 | } 27 | 28 | @keyframes spinner { 29 | 0% { 30 | transform: rotate(0deg); 31 | } 32 | 100% { 33 | transform: rotate(360deg); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/styles/base.scss: -------------------------------------------------------------------------------- 1 | @import './variable.scss'; 2 | 3 | body { 4 | color: var(--ygm-color-text); 5 | font-size: var(--ygm-font-size-m); 6 | font-family: var(--ygm-font-family); 7 | } 8 | 9 | a, 10 | button { 11 | cursor: pointer; 12 | } 13 | 14 | a { 15 | color: var(--ygm-color-primary); 16 | transition: opacity ease-in-out 0.2s; 17 | } 18 | a:active { 19 | opacity: 0.8; 20 | } 21 | -------------------------------------------------------------------------------- /packages/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import 'reset'; 2 | @import 'base'; 3 | -------------------------------------------------------------------------------- /packages/styles/reset.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div, 4 | span, 5 | applet, 6 | object, 7 | iframe, 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6, 14 | p, 15 | blockquote, 16 | pre, 17 | a, 18 | abbr, 19 | acronym, 20 | address, 21 | big, 22 | cite, 23 | code, 24 | del, 25 | dfn, 26 | em, 27 | img, 28 | ins, 29 | kbd, 30 | q, 31 | s, 32 | samp, 33 | small, 34 | strike, 35 | strong, 36 | sub, 37 | sup, 38 | tt, 39 | var, 40 | b, 41 | u, 42 | i, 43 | center, 44 | dl, 45 | dt, 46 | dd, 47 | ol, 48 | ul, 49 | li, 50 | fieldset, 51 | form, 52 | label, 53 | legend, 54 | table, 55 | caption, 56 | tbody, 57 | tfoot, 58 | thead, 59 | tr, 60 | th, 61 | td, 62 | article, 63 | aside, 64 | canvas, 65 | details, 66 | embed, 67 | figure, 68 | figcaption, 69 | footer, 70 | header, 71 | hgroup, 72 | menu, 73 | nav, 74 | output, 75 | ruby, 76 | section, 77 | summary, 78 | time, 79 | mark, 80 | audio, 81 | video { 82 | margin: 0; 83 | padding: 0; 84 | border: 0; 85 | font-size: 100%; 86 | font: inherit; 87 | vertical-align: baseline; 88 | } 89 | /* HTML5 display-role reset for older browsers */ 90 | article, 91 | aside, 92 | details, 93 | figcaption, 94 | figure, 95 | footer, 96 | header, 97 | hgroup, 98 | menu, 99 | nav, 100 | section { 101 | display: block; 102 | } 103 | body { 104 | line-height: 1; 105 | } 106 | ol, 107 | ul { 108 | list-style: none; 109 | } 110 | blockquote, 111 | q { 112 | quotes: none; 113 | } 114 | blockquote:before, 115 | blockquote:after, 116 | q:before, 117 | q:after { 118 | content: ''; 119 | content: none; 120 | } 121 | table { 122 | border-collapse: collapse; 123 | border-spacing: 0; 124 | } 125 | -------------------------------------------------------------------------------- /packages/styles/variable.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | // Color 3 | --ygm-color-primary: #1989fa; 4 | --ygm-color-success: #00b578; 5 | --ygm-color-warning: #ff8f1f; 6 | --ygm-color-danger: #ff3141; 7 | 8 | --ygm-color-white: #ffffff; 9 | --ygm-color-text: #333333; 10 | --ygm-color-weak: #999999; 11 | --ygm-color-light: #cccccc; 12 | --ygm-color-border: #eeeeee; 13 | --ygm-color-background: #ffffff; 14 | --ygm-color-box: #f5f5f5; 15 | 16 | // Padding 17 | --ygm-padding-xs: 4px; 18 | --ygm-padding-s: 8px; 19 | --ygm-padding-m: 12px; 20 | --ygm-padding-l: 16px; 21 | --ygm-padding-xl: 20px; 22 | --ygm-padding-xxl: 24px; 23 | 24 | // Border-radius 25 | --ygm-radius-xs: 4px; 26 | --ygm-radius-s: 6px; 27 | --ygm-radius-m: 8px; 28 | --ygm-radius-l: 10px; 29 | --ygm-radius-xl: 12px; 30 | --ygm-radius-xxl: 14px; 31 | --ygm-radius-xxxl: 16px; 32 | 33 | // Font 34 | --ygm-font-size-xs: 10px; 35 | --ygm-font-size-s: 12px; 36 | --ygm-font-size-m: 14px; 37 | --ygm-font-size-l: 16px; 38 | --ygm-font-size-xl: 18px; 39 | --ygm-font-size-xxl: 20px; 40 | --ygm-font-size-xxxl: 22px; 41 | 42 | --ygm-font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto, 43 | 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif; 44 | } 45 | -------------------------------------------------------------------------------- /packages/swiper/index.tsx: -------------------------------------------------------------------------------- 1 | import InternalSwiper from './swiper'; 2 | import SwiperItem from './swiper-item'; 3 | 4 | export type { SwiperProps, SwiperRef } from './swiper'; 5 | export type { SwiperItemProps } from './swiper-item'; 6 | 7 | type InternalSwiperType = typeof InternalSwiper; 8 | 9 | export interface SwiperInterface extends InternalSwiperType { 10 | Item: typeof SwiperItem; 11 | } 12 | 13 | const Swiper = InternalSwiper as SwiperInterface; 14 | 15 | Swiper.Item = SwiperItem; 16 | 17 | export default Swiper; 18 | -------------------------------------------------------------------------------- /packages/swiper/styles/swiper-item.scss: -------------------------------------------------------------------------------- 1 | .ygm-swiper-item { 2 | display: block; 3 | width: 100%; 4 | height: 100%; 5 | white-space: normal; 6 | } 7 | -------------------------------------------------------------------------------- /packages/swiper/styles/swiper-page-indicator.scss: -------------------------------------------------------------------------------- 1 | .ygm-swiper-page-indicator { 2 | display: flex; 3 | width: auto; 4 | 5 | &-dot { 6 | width: 5px; 7 | height: 5px; 8 | border-radius: 50%; 9 | background-color: var(--ygm-color-weak); 10 | margin-right: 5px; 11 | 12 | &:last-child { 13 | margin-right: 0; 14 | } 15 | 16 | &-active { 17 | width: 13px; 18 | height: 5px; 19 | border-radius: 2px; 20 | background-color: var(--ygm-color-primary); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/swiper/styles/swiper.scss: -------------------------------------------------------------------------------- 1 | .ygm-swiper { 2 | --height: auto; 3 | --width: 100%; 4 | --border-radius: 0; 5 | --track-padding: 0; 6 | 7 | width: var(--width); 8 | height: var(--height); 9 | position: relative; 10 | touch-action: pan-y; 11 | border-radius: var(--border-radius); 12 | overflow: hidden; 13 | z-index: 0; 14 | 15 | &-track { 16 | width: 100%; 17 | height: 100%; 18 | position: relative; 19 | flex-wrap: nowrap; 20 | display: flex; 21 | overflow: hidden; 22 | box-sizing: border-box; 23 | padding: var(--track-padding); 24 | 25 | &-inner { 26 | width: 100%; 27 | height: 100%; 28 | position: relative; 29 | flex-wrap: nowrap; 30 | display: flex; 31 | overflow: hidden; 32 | } 33 | } 34 | 35 | &-slide { 36 | width: 100%; 37 | position: relative; 38 | display: block; 39 | flex-shrink: 0; 40 | white-space: unset; 41 | } 42 | 43 | &-indicator { 44 | position: absolute; 45 | bottom: 6px; 46 | left: 50%; 47 | transform: translateX(-50%); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/swiper/swiper-item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './styles/swiper-item.scss'; 4 | 5 | export interface SwiperItemProps { 6 | onClick?: (e: React.MouseEvent) => void; 7 | children?: React.ReactNode; 8 | } 9 | 10 | const SwiperItem: React.FC = React.memo((props) => { 11 | return ( 12 |
13 | {props.children} 14 |
15 | ); 16 | }); 17 | 18 | SwiperItem.displayName = 'SwiperItem'; 19 | 20 | export default SwiperItem; 21 | -------------------------------------------------------------------------------- /packages/swiper/swiper-page-indicator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import './styles/swiper-page-indicator.scss'; 5 | 6 | export interface SwiperPageIndicatorProps { 7 | current: number; 8 | total: number; 9 | indicatorClassName?: string; 10 | } 11 | 12 | const classPrefix = 'ygm-swiper-page-indicator'; 13 | 14 | const SwiperPageIndicator: React.FC = React.memo((props) => { 15 | const dots: React.ReactElement[] = React.useMemo(() => { 16 | return Array(props.total) 17 | .fill(0) 18 | .map((_, index) => ( 19 |
25 | )); 26 | }, [props]); 27 | 28 | return
{dots}
; 29 | }); 30 | 31 | SwiperPageIndicator.displayName = 'SwiperPageIndicator'; 32 | 33 | export default SwiperPageIndicator; 34 | -------------------------------------------------------------------------------- /packages/swiper/utils.ts: -------------------------------------------------------------------------------- 1 | export const modulus = (value: number, division: number) => { 2 | const remainder = value % division; 3 | return remainder < 0 ? remainder + division : remainder; 4 | }; 5 | -------------------------------------------------------------------------------- /packages/tabs/index.tsx: -------------------------------------------------------------------------------- 1 | import InternalTabs from '@/tabs/tabs'; 2 | import Tab from '@/tabs/tab'; 3 | 4 | export type { TabsProps } from '@/tabs/tabs'; 5 | export type { TabProps } from '@/tabs/tab'; 6 | 7 | type InternalTabsType = typeof InternalTabs; 8 | 9 | export interface TabsInterface extends InternalTabsType { 10 | Tab: typeof Tab; 11 | } 12 | 13 | const Tabs = InternalTabs as TabsInterface; 14 | 15 | Tabs.Tab = Tab; 16 | 17 | export default Tabs; 18 | -------------------------------------------------------------------------------- /packages/tabs/styles/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix-tabs: 'ygm-tabs'; 2 | 3 | .#{$class-prefix-tabs} { 4 | position: relative; 5 | user-select: none; 6 | 7 | &-tab-list { 8 | display: flex; 9 | flex-wrap: nowrap; 10 | justify-content: flex-start; 11 | align-items: center; 12 | position: relative; 13 | overflow-x: scroll; 14 | scrollbar-width: none; 15 | &::-webkit-scrollbar { 16 | display: none; 17 | } 18 | 19 | &-card { 20 | .#{$class-prefix-tabs}-tab-active { 21 | background: var(--ygm-color-primary); 22 | 23 | .#{$class-prefix-tabs}-tab-title { 24 | color: var(--ygm-color-white); 25 | } 26 | } 27 | } 28 | 29 | &-card { 30 | background-color: var(--ygm-color-border); 31 | .#{$class-prefix-tabs}-tab-title { 32 | color: var(--ygm-color-weak); 33 | } 34 | } 35 | } 36 | 37 | &-tab { 38 | flex: auto; 39 | padding: 0 12px; 40 | box-sizing: border-box; 41 | 42 | &-title { 43 | margin: auto; 44 | font-size: var(--ygm-font-size-m); 45 | white-space: nowrap; 46 | width: min-content; 47 | position: relative; 48 | padding: 8px 0 10px; 49 | } 50 | 51 | &-active { 52 | .#{$class-prefix-tabs}-tab-title { 53 | color: var(--ygm-color-primary); 54 | } 55 | } 56 | } 57 | 58 | &-tab-line { 59 | position: absolute; 60 | bottom: 0; 61 | height: 2px; 62 | background: var(--ygm-color-primary); 63 | border-radius: 2px; 64 | } 65 | 66 | &-content { 67 | padding: var(--ygm-padding-m); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/tabs/tab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface TabProps { 4 | key: string; 5 | title: string; 6 | children?: React.ReactNode; 7 | } 8 | 9 | const Tab: React.FC = (props) => { 10 | return props.children ? (props.children as React.ReactElement) : null; 11 | }; 12 | 13 | Tab.displayName = 'Tab'; 14 | 15 | export default Tab; 16 | -------------------------------------------------------------------------------- /packages/tabs/tabs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import Tab from '@/tabs/tab'; 5 | import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect'; 6 | import useUpdateIsomorphicLayoutEffect from '@/hooks/useUpdateIsomorphicLayoutEffect'; 7 | 8 | import { traverseReactNode } from '@/utils/traverse-react-node'; 9 | 10 | import './styles/index.scss'; 11 | 12 | export interface TabsProps { 13 | /** 当前激活tab面板的key */ 14 | activeKey: string; 15 | children?: React.ReactNode; 16 | /** 是否显示tab下划线 */ 17 | showTabLine?: boolean; 18 | /** tab展示形式 */ 19 | type?: 'line' | 'card'; 20 | /** 点击tab切换后回调 */ 21 | onChange?: (key: string) => void; 22 | /** 激活的tab样式 */ 23 | tabActiveClassName?: string; 24 | /** tab列表样式 */ 25 | tabListClassName?: string; 26 | /** tab内容样式 */ 27 | tabContentClassName?: string; 28 | } 29 | 30 | const classPrefix = 'ygm-tabs'; 31 | 32 | const Tabs: React.FC = (props) => { 33 | const [activeKey, setActiveKey] = React.useState(props.activeKey); 34 | const [activeLineStyle, setActiveLineStyle] = React.useState({ 35 | width: 0, 36 | transform: `translate3d(0px, 0px, 0px)`, 37 | transitionDuration: '0', 38 | }); 39 | const tabListRef = React.useRef(null); 40 | 41 | const keyToIndexRecord: Record = React.useMemo(() => ({}), []); 42 | const panes: React.ReactElement>[] = []; 43 | 44 | traverseReactNode(props.children, (child) => { 45 | if (!React.isValidElement(child)) return; 46 | if (!child.key) return; 47 | const length = panes.push(child); 48 | keyToIndexRecord[child.key] = length - 1; 49 | }); 50 | 51 | const onTab = React.useCallback( 52 | (e: React.MouseEvent) => { 53 | const key = (e.target as HTMLElement).dataset['key'] as string; 54 | setActiveKey(key); 55 | props?.onChange?.(key); 56 | }, 57 | [props?.onChange] 58 | ); 59 | 60 | const calculateLineWidth = React.useCallback( 61 | (immediate = false) => { 62 | if (!props.showTabLine) return; 63 | const tabListEle = tabListRef.current; 64 | if (!tabListEle) return; 65 | const activeIndex = keyToIndexRecord[activeKey]; 66 | const activeTabWrapper = tabListRef.current.children.item(activeIndex + 1) as HTMLDivElement; 67 | const activeTab = activeTabWrapper.children.item(0) as HTMLDivElement; 68 | const activeTabWidth = activeTab.offsetWidth; 69 | const activeTabLeft = activeTab.offsetLeft; 70 | const width = activeTabWidth; 71 | const x = activeTabLeft; 72 | 73 | setActiveLineStyle({ 74 | width, 75 | transform: `translate3d(${x}px, 0px, 0px)`, 76 | transitionDuration: immediate ? '0ms' : '300ms', 77 | }); 78 | }, 79 | [activeKey, keyToIndexRecord, props.showTabLine] 80 | ); 81 | 82 | useIsomorphicLayoutEffect(() => { 83 | calculateLineWidth(true); 84 | }, []); 85 | 86 | useUpdateIsomorphicLayoutEffect(() => { 87 | calculateLineWidth(); 88 | }, [calculateLineWidth]); 89 | 90 | React.useEffect(() => { 91 | window.addEventListener('resize', () => calculateLineWidth(true)); 92 | 93 | return () => window.removeEventListener('resize', () => calculateLineWidth(true)); 94 | }, [calculateLineWidth]); 95 | 96 | return ( 97 |
98 |
104 | {props.showTabLine && ( 105 |
111 | )} 112 | {panes.map((item) => ( 113 |
121 |
122 | {item.props.title} 123 |
124 |
125 | ))} 126 |
127 | 128 | {panes.map( 129 | (child) => 130 | child.props.children && ( 131 |
136 | {child} 137 |
138 | ) 139 | )} 140 |
141 | ); 142 | }; 143 | 144 | Tabs.defaultProps = { 145 | showTabLine: true, 146 | type: 'line', 147 | }; 148 | 149 | Tabs.displayName = 'Tabs'; 150 | 151 | export default Tabs; 152 | -------------------------------------------------------------------------------- /packages/toast/index.tsx: -------------------------------------------------------------------------------- 1 | export type { ToastShowProps } from './methods'; 2 | 3 | import { show } from './methods'; 4 | 5 | export interface ToastProps { 6 | show: typeof show; 7 | } 8 | 9 | const Toast = { 10 | show, 11 | }; 12 | 13 | export default Toast; 14 | -------------------------------------------------------------------------------- /packages/toast/methods.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | 4 | import { default as Toast, ToastProps } from './toast'; 5 | 6 | export type ToastShowProps = ToastProps; 7 | 8 | export const show = (p: ToastShowProps | string) => { 9 | const props = typeof p === 'string' ? { content: p } : p; 10 | 11 | const container = document.createElement('div'); 12 | document.body.appendChild(container); 13 | 14 | const root = ReactDOM.createRoot(container); 15 | 16 | const unmount = () => { 17 | document.body.removeChild(container); 18 | root.unmount(); 19 | }; 20 | 21 | root.render(); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/toast/styles/index.scss: -------------------------------------------------------------------------------- 1 | .ygm-toast { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | user-select: none; 8 | z-index: 1001; 9 | 10 | &-main { 11 | display: inline-block; 12 | position: relative; 13 | top: 50%; 14 | left: 50%; 15 | transform: translate(-50%, -50%); 16 | width: auto; 17 | min-width: 96px; 18 | max-width: 70%; 19 | max-height: 70%; 20 | overflow: auto; 21 | color: var(--ygm-color-white); 22 | word-break: break-all; 23 | background-color: rgba(0, 0, 0, 0.7); 24 | border-radius: var(--ygm-radius-m); 25 | pointer-events: all; 26 | font-size: var(--ygm-font-size-m); 27 | line-height: 1.5; 28 | box-sizing: border-box; 29 | 30 | &-icon { 31 | padding: 30px 35px; 32 | } 33 | 34 | &-text { 35 | padding: var(--ygm-padding-m); 36 | } 37 | } 38 | 39 | &-text { 40 | text-align: center; 41 | } 42 | 43 | &-icon { 44 | text-align: center; 45 | margin-bottom: 8px; 46 | font-size: 36px; 47 | line-height: 1; 48 | display: flex; 49 | justify-content: center; 50 | align-items: center; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/toast/toast.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | import { CheckOutline, CloseOutline } from 'antd-mobile-icons'; 4 | 5 | import SpinnerLoading from '@/spinner-loading'; 6 | 7 | import './styles/index.scss'; 8 | 9 | export interface ToastProps { 10 | /** 提示持续时间 */ 11 | duration?: number; 12 | /** Toast文本内容 */ 13 | content: React.ReactNode; 14 | /** Toast关闭后的回调 */ 15 | afterClose?: () => void; 16 | /** 卸载当前Toast的DOM */ 17 | unmount?: () => void; 18 | /** Toast图标 */ 19 | icon?: 'success' | 'fail' | 'loading' | React.ReactNode; 20 | } 21 | 22 | const classPrefix = 'ygm-toast'; 23 | 24 | const Toast: React.FC = React.memo(({ icon, duration, content, afterClose, unmount }) => { 25 | const [_, setVisible] = React.useState(true); 26 | 27 | const iconElement = React.useMemo(() => { 28 | if (icon === null || icon === undefined) return null; 29 | switch (icon) { 30 | case 'success': 31 | return ; 32 | case 'fail': 33 | return ; 34 | case 'loading': 35 | return ; 36 | default: 37 | return icon; 38 | } 39 | }, [icon]); 40 | 41 | React.useEffect(() => { 42 | const timer = window.setTimeout(() => { 43 | setVisible(false); 44 | unmount?.(); 45 | }, duration); 46 | 47 | return () => clearTimeout(timer); 48 | }, [duration, unmount]); 49 | 50 | React.useEffect(() => { 51 | return () => { 52 | afterClose?.(); 53 | }; 54 | }, [afterClose]); 55 | 56 | return ( 57 |
58 |
59 | {iconElement &&
{iconElement}
} 60 |
{content}
61 |
62 |
63 | ); 64 | }); 65 | 66 | Toast.defaultProps = { 67 | duration: 2000, 68 | }; 69 | 70 | Toast.displayName = 'Toast'; 71 | 72 | export default Toast; 73 | -------------------------------------------------------------------------------- /packages/utils/event.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const getTouchEventData = ( 4 | e: TouchEvent | MouseEvent | React.TouchEvent | React.MouseEvent 5 | ) => { 6 | return 'changedTouches' in e ? e.changedTouches[0] : e; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/utils/render-imperatively.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderToBody } from '@/utils/render'; 3 | 4 | export interface ElementProps { 5 | visible?: boolean; 6 | onClose?: () => void; 7 | afterClose?: () => void; 8 | } 9 | 10 | const renderImperatively = (element: React.ReactElement) => { 11 | const Wraper = () => { 12 | const [visible, setVisible] = React.useState(false); 13 | 14 | const onClose = () => { 15 | element.props?.onClose?.(); 16 | setVisible(false); 17 | }; 18 | 19 | const afterClose = () => { 20 | unmount(); 21 | }; 22 | 23 | React.useEffect(() => { 24 | setVisible(true); 25 | }, []); 26 | 27 | return React.cloneElement(element, { ...element.props, visible, onClose, afterClose }); 28 | }; 29 | 30 | const unmount = renderToBody(); 31 | }; 32 | 33 | export default renderImperatively; 34 | -------------------------------------------------------------------------------- /packages/utils/render.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | 4 | export const render = (element: React.ReactElement, container: HTMLElement) => { 5 | const root = ReactDOM.createRoot(container); 6 | root.render(element); 7 | 8 | const unmount = () => { 9 | document.body.removeChild(container); 10 | root.unmount(); 11 | }; 12 | 13 | return unmount; 14 | }; 15 | 16 | export const renderToBody = (element: React.ReactElement) => { 17 | const container = document.createElement('div'); 18 | document.body.appendChild(container); 19 | 20 | const unmount = render(element, container); 21 | 22 | return unmount; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/utils/scroll.ts: -------------------------------------------------------------------------------- 1 | const inBrowser = typeof window !== 'undefined'; 2 | 3 | const defaultRoot = inBrowser ? window : undefined; 4 | 5 | type ScrollElement = HTMLElement | Window; 6 | 7 | const overflowStylePatterns = ['scroll', 'auto']; 8 | 9 | const isElement = (node: HTMLElement) => { 10 | const ELEMENT_NODE_TYPE = 1; 11 | return node.tagName !== 'HTML' && node.tagName !== 'BODY' && node.nodeType === ELEMENT_NODE_TYPE; 12 | }; 13 | 14 | export const getScrollParent = (el: HTMLElement, root: ScrollElement | undefined = defaultRoot) => { 15 | let node = el; 16 | 17 | while (node && node !== root && isElement(node)) { 18 | const { overflowY } = window.getComputedStyle(node); 19 | if (overflowStylePatterns.includes(overflowY) && node.scrollHeight > node.clientHeight) { 20 | return node; 21 | } 22 | node = node.parentNode as HTMLElement; 23 | } 24 | 25 | return root; 26 | }; 27 | 28 | export const getScrollTop = (element: Window | Element) => { 29 | const top = 'scrollTop' in element ? element.scrollTop : element.scrollY; 30 | 31 | // iOS scroll bounce cause minus scrollTop 32 | return Math.max(top, 0); 33 | }; 34 | 35 | export const sleep = (time: number) => 36 | new Promise((resolve) => { 37 | window.setTimeout(resolve, time); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/utils/traverse-react-node.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { isFragment } from 'react-is'; 3 | 4 | export const traverseReactNode = (children: React.ReactNode, fn: (child: React.ReactNode, index: number) => void) => { 5 | let i = 0; 6 | const handle = (target: React.ReactNode) => { 7 | React.Children.forEach(target, (child) => { 8 | if (!isFragment(child)) { 9 | fn(child, i); 10 | i++; 11 | } else { 12 | handle(child.props.children); 13 | } 14 | }); 15 | }; 16 | 17 | handle(children); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export const getValueByScope = (value: number, min: number, max: number): number => { 2 | let newValue = Math.max(value, min); 3 | newValue = Math.min(newValue, max); 4 | return newValue; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/utils/validate.ts: -------------------------------------------------------------------------------- 1 | export function isPromise(obj: unknown): obj is Promise { 2 | return !!obj && typeof obj === 'object' && typeof (obj as any).then === 'function'; 3 | } 4 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const log = require('./utils/log'); 2 | const babel = require('./utils/babel'); 3 | 4 | async function build() { 5 | const cwd = process.cwd(); 6 | 7 | const bundleOpts = { 8 | entry: 'packages/index.tsx', 9 | }; 10 | 11 | log('Build cjs with babel'); 12 | await babel({ cwd, type: 'cjs', bundleOpts }); 13 | 14 | log('Build esm with babel'); 15 | await babel({ cwd, type: 'esm', importLibToEs: true, bundleOpts }); 16 | } 17 | 18 | build(); 19 | -------------------------------------------------------------------------------- /scripts/utils/babel.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const babel = require('@babel/core'); 4 | const chalk = require('chalk'); 5 | const gulpIf = require('gulp-if'); 6 | const gulpAlias = require('gulp-ts-alias'); 7 | const gulpTs = require('gulp-typescript'); 8 | const gulpStyle = require('gulp-style-aliases'); 9 | const gulpSass = require('gulp-sass')(require('sass')); 10 | const slash = require('slash2'); 11 | const rimraf = require('rimraf'); 12 | const through = require('through2'); 13 | const vfs = require('vinyl-fs'); 14 | const signale = require('signale'); 15 | const getBabelConfig = require('./getBabelConfig'); 16 | const log = require('./log'); 17 | 18 | module.exports = function (opts) { 19 | const { cwd, type } = opts; 20 | 21 | const srcPath = path.join(cwd, 'packages'); 22 | const targetDir = type === 'esm' ? 'es' : 'lib'; 23 | const targetPath = path.join(cwd, targetDir); 24 | 25 | log(chalk.gray(`Clean ${targetDir} directory`)); 26 | rimraf.sync(targetPath); 27 | 28 | const tsConfigPath = path.join(cwd, 'tsconfig.json'); 29 | 30 | const tsConfig = JSON.parse(fs.readFileSync(tsConfigPath, 'utf-8')).compilerOptions || {}; 31 | function isTsFile(path) { 32 | return /\.tsx?$/.test(path) && !path.endsWith('.d.ts'); 33 | } 34 | 35 | function isStyleFile(path) { 36 | return /\._?(css|scss)?$/.test(path); 37 | } 38 | 39 | function isTransform(path) { 40 | const babelTransformRegexp = /\.(t|j)sx?$/; 41 | return babelTransformRegexp.test(path) && !path.endsWith('.d.ts'); 42 | } 43 | 44 | function transform(opts) { 45 | const { file, type } = opts; 46 | const babelOptions = getBabelConfig(type); 47 | const relFile = slash(file.path).replace(`${cwd}/`, ''); 48 | log(`Transform to ${type} for ${chalk['yellow'](relFile)}`); 49 | 50 | return babel.transform(file.contents, { 51 | ...babelOptions, 52 | filename: file.path, 53 | }).code; 54 | } 55 | 56 | function createStream(src) { 57 | return vfs 58 | .src(src, { 59 | allowEmpty: true, 60 | base: srcPath, 61 | }) 62 | .pipe(gulpIf((f) => isTsFile(f.path), gulpAlias({ configuration: tsConfig }))) 63 | .pipe(gulpIf((f) => isStyleFile(f.path), gulpAlias({ configuration: tsConfig }))) 64 | .pipe( 65 | gulpIf( 66 | (f) => isStyleFile(f.path), 67 | gulpStyle({ 68 | '@': 'packages', 69 | }) 70 | ) 71 | ) 72 | .pipe(gulpIf((f) => isStyleFile(f.path), gulpSass())) 73 | .pipe( 74 | gulpIf( 75 | (f) => isTransform(f.path), 76 | through.obj((file, env, cb) => { 77 | try { 78 | file.contents = Buffer.from( 79 | transform({ 80 | file, 81 | type, 82 | }) 83 | ); 84 | // .jsx -> .js 85 | file.path = file.path.replace(path.extname(file.path), '.js'); 86 | cb(null, file); 87 | } catch (e) { 88 | signale.error(`Compiled faild: ${file.path}`); 89 | console.log(e); 90 | cb(null); 91 | } 92 | }) 93 | ) 94 | ) 95 | .pipe(vfs.dest(targetPath)); 96 | } 97 | 98 | function createTypeStream(src) { 99 | return vfs 100 | .src(src, { 101 | allowEmpty: true, 102 | base: srcPath, 103 | }) 104 | .pipe(gulpIf((f) => isTsFile(f.path), gulpAlias({ configuration: tsConfig }))) 105 | .pipe(gulpIf((f) => isTsFile(f.path), gulpTs({ ...tsConfig, files: [path.join(cwd, 'typings.d.ts')] }))) 106 | .pipe(vfs.dest(targetPath)); 107 | } 108 | 109 | const patterns = [path.join(srcPath, '**/*')]; 110 | 111 | return new Promise((resolve) => { 112 | createTypeStream(patterns).on('end', resolve); 113 | }).then( 114 | () => 115 | new Promise((resolve) => { 116 | createStream(patterns).on('end', resolve); 117 | }) 118 | ); 119 | }; 120 | -------------------------------------------------------------------------------- /scripts/utils/getBabelConfig.js: -------------------------------------------------------------------------------- 1 | function getBabelConfig(type) { 2 | return { 3 | presets: [ 4 | [ 5 | '@babel/preset-env', 6 | { 7 | modules: type === 'esm' ? false : 'commonjs', 8 | targets: { 9 | browsers: ['> 1%', 'last 2 versions', 'not dead'], 10 | }, 11 | }, 12 | ], 13 | '@babel/preset-typescript', 14 | '@babel/preset-react', 15 | ], 16 | plugins: [['@babel/plugin-transform-runtime', { corejs: 3 }]], 17 | }; 18 | } 19 | 20 | module.exports = getBabelConfig; 21 | -------------------------------------------------------------------------------- /scripts/utils/log.js: -------------------------------------------------------------------------------- 1 | const randomColor = require('./randomColor'); 2 | 3 | module.exports = function (msg) { 4 | console.log(`${randomColor('@yg/react-mobile-ui')} ${msg}`); 5 | }; 6 | -------------------------------------------------------------------------------- /scripts/utils/randomColor.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | const colors = [ 4 | 'red', 5 | 'green', 6 | 'yellow', 7 | 'blue', 8 | 'magenta', 9 | 'cyan', 10 | 'gray', 11 | 'redBright', 12 | 'greenBright', 13 | 'yellowBright', 14 | 'blueBright', 15 | 'magentaBright', 16 | 'cyanBright', 17 | ]; 18 | 19 | let index = 0; 20 | const cache = {}; 21 | 22 | module.exports = function (pkg) { 23 | if (!cache[pkg]) { 24 | const color = colors[index]; 25 | let str = chalk[color].bold(pkg); 26 | cache[pkg] = str; 27 | if (index === colors.length - 1) { 28 | index = 0; 29 | } else { 30 | index += 1; 31 | } 32 | } 33 | return cache[pkg]; 34 | }; 35 | -------------------------------------------------------------------------------- /stories/action-sheet/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoyage/react-mobile-ui/e0d17002078e43a0fdfc75407f78b3e937e4127d/stories/action-sheet/index.scss -------------------------------------------------------------------------------- /stories/action-sheet/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Meta } from '@storybook/react'; 4 | 5 | import ActionSheet, { Action } from '@/action-sheet'; 6 | import Button from '@/button'; 7 | import Space from '@/space'; 8 | import Toast from '@/toast'; 9 | 10 | import DemoWrap from '../../demos/demo-wrap'; 11 | import DemoBlock from '../../demos/demo-block'; 12 | 13 | const ActionSheetStory: Meta = { 14 | title: '反馈/ActionSheet 动作面板', 15 | component: ActionSheet, 16 | }; 17 | 18 | const actions: Action[] = [ 19 | { name: '选项1', key: 'option1' }, 20 | { name: '选项2', key: 'option2' }, 21 | { name: '选项3', key: 'option3' }, 22 | ]; 23 | 24 | const actions1: Action[] = [ 25 | { name: '选项一', key: 'option1' }, 26 | { name: '选项二', key: 'option2' }, 27 | { name: '选项三', description: '描述信息', key: 'option3' }, 28 | ]; 29 | 30 | const actions2: Action[] = [ 31 | { name: '选项一', key: 'option1', color: '#ee0a24' }, 32 | { name: '选项二', key: 'option2', disabled: true }, 33 | { name: '选项三', description: '描述信息', key: 'option3' }, 34 | ]; 35 | 36 | export const Basic = () => { 37 | const [visible1, setVisible1] = React.useState(false); 38 | const [visible2, setVisible2] = React.useState(false); 39 | const [visible3, setVisible3] = React.useState(false); 40 | const [visible4, setVisible4] = React.useState(false); 41 | const [visible5, setVisible5] = React.useState(false); 42 | 43 | return ( 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | {/* 基本用法 */} 62 | setVisible1(false)} /> 63 | 64 | {/* 展示取消按钮 */} 65 | setVisible2(false)} cancelText="取消" /> 66 | 67 | {/* 展示描述信息 */} 68 | setVisible3(false)} 72 | cancelText="取消" 73 | description="这是一段描述信息" 74 | /> 75 | 76 | {/* 选项状态 */} 77 | setVisible4(false)} cancelText="取消" /> 78 | 79 | {/* 事件处理 */} 80 | { 84 | Toast.show(`点击了${action.name}`); 85 | }} 86 | onClose={() => { 87 | setVisible5(false); 88 | Toast.show('动作面板已关闭'); 89 | }} 90 | cancelText="取消" 91 | /> 92 | 93 | ); 94 | }; 95 | 96 | Basic.storyName = '基本用法'; 97 | 98 | export default ActionSheetStory; 99 | -------------------------------------------------------------------------------- /stories/button/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Meta } from '@storybook/react'; 4 | 5 | import Button from '@/button'; 6 | import Space from '@/space'; 7 | 8 | import DemoWrap from '../../demos/demo-wrap'; 9 | import DemoBlock from '../../demos/demo-block'; 10 | 11 | const ButtonStory: Meta = { 12 | title: '通用/Button 按钮', 13 | component: Button, 14 | }; 15 | 16 | export const Basic = () => { 17 | return ( 18 | 19 | 20 | 21 | 24 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 47 | 48 | 49 | 50 | 51 | 54 | 57 | 60 | 61 | 62 | 63 | 64 | 65 | 68 | 71 | 74 | 75 | 76 | 77 | ); 78 | }; 79 | 80 | Basic.storyName = '基本用法'; 81 | 82 | export default ButtonStory; 83 | -------------------------------------------------------------------------------- /stories/card/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Meta } from '@storybook/react'; 4 | import { RightOutline } from 'antd-mobile-icons'; 5 | 6 | import Card from '@/card'; 7 | 8 | import DemoWrap from '../../demos/demo-wrap'; 9 | import DemoBlock from '../../demos/demo-block'; 10 | 11 | const CardStory: Meta = { 12 | title: '信息展示/Card 卡片', 13 | component: Card, 14 | }; 15 | 16 | export const Basic = () => { 17 | return ( 18 | 19 | 20 | 内容 21 | 22 | 23 | 24 | 25 | 26 | 27 | 内容 28 | 29 | 30 | 31 | }> 32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | Basic.storyName = '基本用法'; 48 | 49 | export default CardStory; 50 | -------------------------------------------------------------------------------- /stories/cell/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | import { UnorderedListOutline, PayCircleOutline } from 'antd-mobile-icons'; 4 | 5 | import Cell from '@/cell'; 6 | import Space from '@/space'; 7 | 8 | import DemoWrap from '../../demos/demo-wrap'; 9 | 10 | const CellStory: Meta = { 11 | title: '信息展示/Cell 单元格', 12 | component: Cell, 13 | subcomponents: { 'Cell.Group': Cell.Group }, 14 | }; 15 | 16 | export const Basic = () => { 17 | return ( 18 | 19 | 20 | 21 | 内容 22 | 23 | 内容 24 | 25 | 26 | 27 | 28 | 内容 29 | 30 | 内容 31 | 32 | 33 | 34 | 35 | }> 36 | 内容 37 | 38 | } /> 39 | 40 | 41 | 42 | 43 | 内容 44 | 45 | 46 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | Basic.storyName = '基本用法'; 53 | 54 | export default CellStory; 55 | -------------------------------------------------------------------------------- /stories/countdown/index.scss: -------------------------------------------------------------------------------- 1 | .demo-countdown-num { 2 | color: #fff; 3 | width: 16px; 4 | height: 16px; 5 | border-radius: 2px; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | box-sizing: border-box; 10 | background: #ee0a24; 11 | padding: 10px; 12 | } 13 | 14 | .demo-countdown-symbol { 15 | color: #ee0a24; 16 | height: 14px; 17 | margin: 0 2px; 18 | vertical-align: middle; 19 | display: inline-block; 20 | } 21 | -------------------------------------------------------------------------------- /stories/countdown/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import Countdown from '@/countdown'; 5 | 6 | import DemoWrap from '../../demos/demo-wrap'; 7 | import DemoBlock from '../../demos/demo-block'; 8 | 9 | import './index.scss'; 10 | 11 | const CountdownStory: Meta = { 12 | title: '信息展示/Countdown 倒计时', 13 | component: Countdown, 14 | }; 15 | 16 | export const Basic = () => { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | Basic.storyName = '基本用法'; 40 | 41 | export default CountdownStory; 42 | -------------------------------------------------------------------------------- /stories/dialog/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import Dialog from '@/dialog'; 5 | import Button from '@/button'; 6 | import Space from '@/space'; 7 | 8 | import DemoWrap from '../../demos/demo-wrap'; 9 | import DemoBlock from '../../demos/demo-block'; 10 | import { sleep } from '@/pull-to-refresh/utils'; 11 | 12 | const DialogStory: Meta = { 13 | title: '反馈/Dialog 弹出框', 14 | component: Dialog, 15 | }; 16 | 17 | export const Basic = () => { 18 | const [visible1, setVisible1] = React.useState(false); 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 48 | 49 | 50 | 51 | 52 | 53 | setVisible1(false)} 58 | closeOnAction 59 | actions={[ 60 | { 61 | key: 'cancel', 62 | text: '取消', 63 | }, 64 | { 65 | key: 'confirm', 66 | text: '确认', 67 | color: 'primary', 68 | }, 69 | ]} 70 | /> 71 | 72 | 73 | ); 74 | }; 75 | 76 | Basic.storyName = '基本用法'; 77 | 78 | export default DialogStory; 79 | -------------------------------------------------------------------------------- /stories/divider/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Meta } from '@storybook/react'; 4 | 5 | import Divider from '@/divider'; 6 | 7 | import DemoWrap from '../../demos/demo-wrap'; 8 | import DemoBlock from '../../demos/demo-block'; 9 | 10 | const GridStory: Meta = { 11 | title: '布局/Divider 分割线', 12 | component: Divider, 13 | }; 14 | 15 | export const Basic = () => { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 文字 24 | 25 | 26 | 27 | 左侧内容位置 28 | 右侧内容位置 29 | 30 | 31 | 32 | 虚线Divider 33 | 34 | 35 | 36 | 37 | 自定义样式 38 | 39 | 40 | 41 | 42 | <> 43 | Text 44 | 45 | Link 46 | 47 | Link 48 | 49 | 50 | 51 | ); 52 | }; 53 | 54 | Basic.storyName = '基本用法'; 55 | 56 | export default GridStory; 57 | -------------------------------------------------------------------------------- /stories/ellipsis/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import Ellipsis from '@/ellipsis'; 5 | 6 | import DemoWrap from '../../demos/demo-wrap'; 7 | import DemoBlock from '../../demos/demo-block'; 8 | 9 | const EllipsisStory: Meta = { 10 | title: '信息展示/Ellipsis 文本省略', 11 | component: Ellipsis, 12 | }; 13 | 14 | export const Basic = () => { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | Basic.storyName = '基本用法'; 47 | 48 | export default EllipsisStory; 49 | -------------------------------------------------------------------------------- /stories/error-block/index.scss: -------------------------------------------------------------------------------- 1 | .demo-block-content { 2 | .ygm-error-block { 3 | height: 100%; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /stories/error-block/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import ErrorBlock from '@/error-block'; 5 | 6 | import DemoWrap from '../../demos/demo-wrap'; 7 | import DemoBlock from '../../demos/demo-block'; 8 | 9 | import './index.scss'; 10 | 11 | const ErrorStory: Meta = { 12 | title: '反馈/ErrorBlock 异常', 13 | component: ErrorBlock, 14 | }; 15 | 16 | export const Basic = () => { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | Basic.storyName = '基本用法'; 35 | 36 | export default ErrorStory; 37 | -------------------------------------------------------------------------------- /stories/grid/index.scss: -------------------------------------------------------------------------------- 1 | .grid-demo-item-block { 2 | border: solid 1px #999999; 3 | background: var(--ygm-color-box); 4 | text-align: center; 5 | color: #999999; 6 | height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /stories/grid/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Meta } from '@storybook/react'; 4 | 5 | import Grid from '@/grid'; 6 | 7 | import DemoWrap from '../../demos/demo-wrap'; 8 | import DemoBlock from '../../demos/demo-block'; 9 | 10 | import './index.scss'; 11 | 12 | const GridStory: Meta = { 13 | title: '布局/Grid 栅格', 14 | component: Grid, 15 | }; 16 | 17 | export const Basic = () => { 18 | return ( 19 | 20 | 21 | 22 | 23 |
A
24 |
25 | 26 |
B
27 |
28 | 29 |
C
30 |
31 | 32 |
D
33 |
34 | 35 |
E
36 |
37 |
38 |
39 | 40 | 41 | 42 |
A
43 |
44 | 45 |
B
46 |
47 | 48 |
C
49 |
50 | 51 |
D
52 |
53 | 54 |
E
55 |
56 |
57 |
58 |
59 | ); 60 | }; 61 | 62 | Basic.storyName = '基本用法'; 63 | 64 | export default GridStory; 65 | -------------------------------------------------------------------------------- /stories/image/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoyage/react-mobile-ui/e0d17002078e43a0fdfc75407f78b3e937e4127d/stories/image/img.png -------------------------------------------------------------------------------- /stories/image/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Meta } from '@storybook/react'; 4 | 5 | import Image from '@/image'; 6 | import Space from '@/space'; 7 | 8 | import DemoWrap from '../../demos/demo-wrap'; 9 | import DemoBlock from '../../demos/demo-block'; 10 | 11 | import demoImg from './img.png'; 12 | 13 | const ImageStory: Meta = { 14 | title: '信息展示/Image 图片', 15 | component: Image, 16 | }; 17 | 18 | export const Basic = () => { 19 | return ( 20 | 21 | 22 | demo 23 | 24 | 25 | 26 | 27 | demo 28 | demo 29 | demo 30 | demo 31 | 32 | 33 | 34 | 35 | 36 | demo 37 | demo 38 | demo 39 | 40 | 41 | 42 | 43 | demo 44 | 45 | 46 | ); 47 | }; 48 | 49 | Basic.storyName = '基本用法'; 50 | 51 | export default ImageStory; 52 | -------------------------------------------------------------------------------- /stories/infinite-scroll/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import { List } from 'antd-mobile'; 5 | 6 | import InfiniteScroll from '@/infinite-scroll'; 7 | import Space from '@/space'; 8 | 9 | import DemoWrap from '../../demos/demo-wrap'; 10 | import DemoBlock from '../../demos/demo-block'; 11 | 12 | const ErrorStory: Meta = { 13 | title: '信息展示/infiniteScroll 无限滚动', 14 | component: InfiniteScroll, 15 | }; 16 | 17 | const mockData = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q']; 18 | 19 | function sleep(ms: number): any { 20 | // eslint-disable-next-line no-promise-executor-return 21 | return new Promise((resolve) => window.setTimeout(resolve, ms)); 22 | } 23 | 24 | export const Basic = () => { 25 | const [data1, setData1] = React.useState(mockData); 26 | const [data2, setData2] = React.useState(mockData); 27 | const [hasMore1, setHasMore1] = React.useState(true); 28 | const [hasMore2, setHasMore2] = React.useState(true); 29 | 30 | const loadMore1 = async () => { 31 | await sleep(3000); 32 | setData1((val) => [...val, ...mockData]); 33 | if (data1.length >= 68) { 34 | setHasMore1(false); 35 | } 36 | }; 37 | 38 | const loadMore2 = async () => { 39 | await sleep(3000); 40 | setData2((val) => [...val, ...mockData]); 41 | if (data2.length >= 68) { 42 | setHasMore2(false); 43 | } 44 | }; 45 | 46 | return ( 47 | 48 | 49 | 50 |
51 | 52 | 53 | {data1.map((item, index) => ( 54 | {item} 55 | ))} 56 | 57 | 58 |
59 |
60 | 61 | 62 |
63 | 加载中 : --- 没有更多 ---} 67 | > 68 | 69 | {data2.map((item, index) => ( 70 | {item} 71 | ))} 72 | 73 | 74 |
75 |
76 |
77 |
78 | ); 79 | }; 80 | 81 | Basic.storyName = '基本用法'; 82 | 83 | export default ErrorStory; 84 | -------------------------------------------------------------------------------- /stories/input/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import Input from '@/input'; 5 | 6 | import DemoWrap from '../../demos/demo-wrap'; 7 | import DemoBlock from '../../demos/demo-block'; 8 | 9 | const InputStory: Meta = { 10 | title: '信息录入/Input 输入框', 11 | component: Input, 12 | }; 13 | 14 | export const Basic = () => { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | Basic.storyName = '基本用法'; 37 | 38 | export default InputStory; 39 | -------------------------------------------------------------------------------- /stories/mask/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import Mask from '@/mask'; 5 | import Button from '@/button'; 6 | 7 | import DemoWrap from '../../demos/demo-wrap'; 8 | import DemoBlock from '../../demos/demo-block'; 9 | 10 | const MaskStory: Meta = { 11 | title: '反馈/Mask 遮罩层', 12 | component: Mask, 13 | }; 14 | 15 | export const Basic = () => { 16 | const [visible1, setVisible1] = React.useState(false); 17 | const [visible2, setVisible2] = React.useState(false); 18 | 19 | return ( 20 | 21 | 22 | 23 | setVisible1(false)} /> 24 | 25 | 26 | 27 | 28 | setVisible2(false)} 32 | /> 33 | 34 | 35 | ); 36 | }; 37 | 38 | Basic.storyName = '基本用法'; 39 | 40 | export default MaskStory; 41 | -------------------------------------------------------------------------------- /stories/nav-bar/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | import { SearchOutline } from 'antd-mobile-icons'; 4 | 5 | import NavBar from '@/nav-bar'; 6 | import Toast from '@/toast'; 7 | 8 | import DemoWrap from '../../demos/demo-wrap'; 9 | import DemoBlock from '../../demos/demo-block'; 10 | 11 | const NavBarStory: Meta = { 12 | title: '导航/NavBar 导航栏', 13 | component: NavBar, 14 | }; 15 | 16 | export const Basic = () => { 17 | const onBack = () => { 18 | Toast.show('back'); 19 | }; 20 | 21 | return ( 22 | 23 | 24 | 标题 25 | 26 | 27 | 28 | 标题 29 | 30 | 31 | 32 | }> 33 | 标题 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | Basic.storyName = '基本用法'; 41 | 42 | export default NavBarStory; 43 | -------------------------------------------------------------------------------- /stories/popup/index.scss: -------------------------------------------------------------------------------- 1 | .popup-demo { 2 | .ygm-button { 3 | margin-right: 20px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /stories/popup/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import Popup from '@/popup'; 5 | import Button from '@/button'; 6 | 7 | import './index.scss'; 8 | 9 | const ToastStory: Meta = { 10 | title: '反馈/Popup 弹出层', 11 | component: Popup, 12 | }; 13 | 14 | export const Basic = () => { 15 | const [visible1, setVisible1] = React.useState(false); 16 | const [visible2, setVisible2] = React.useState(false); 17 | const [visible3, setVisible3] = React.useState(false); 18 | const [visible4, setVisible4] = React.useState(false); 19 | 20 | return ( 21 |
22 | 23 | 24 | 25 | 26 | 27 | setVisible1(false)} style={{ height: '30vh' }} /> 28 | setVisible2(false)} style={{ height: '30vh' }} /> 29 | setVisible3(false)} style={{ width: '30vh' }} /> 30 | setVisible4(false)} style={{ width: '30vh' }} /> 31 |
32 | ); 33 | }; 34 | 35 | Basic.storyName = '基本用法'; 36 | 37 | export default ToastStory; 38 | -------------------------------------------------------------------------------- /stories/pull-to-refresh/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import DemoWrap from '../../demos/demo-wrap'; 5 | import DemoBlock from '../../demos/demo-block'; 6 | 7 | import PullToRefresh from '@/pull-to-refresh'; 8 | 9 | const CountdownStory: Meta = { 10 | title: '反馈/PullToRefresh 下拉刷新', 11 | component: PullToRefresh, 12 | }; 13 | 14 | const list = new Array(20).fill(1); 15 | 16 | export const Basic = () => { 17 | const onRefresh = () => { 18 | return new Promise((resolve) => { 19 | setTimeout(resolve, 3000); 20 | }); 21 | }; 22 | 23 | return ( 24 | 25 | 26 | 27 | {list.map((_, index) => ( 28 |
list-{index}
29 | ))} 30 |
31 |
32 |
33 | ); 34 | }; 35 | 36 | Basic.storyName = '基本用法'; 37 | 38 | export default CountdownStory; 39 | -------------------------------------------------------------------------------- /stories/search-bar/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import SearchBar from '@/search-bar'; 5 | import Toast from '@/toast'; 6 | 7 | import DemoWrap from '../../demos/demo-wrap'; 8 | import DemoBlock from '../../demos/demo-block'; 9 | 10 | const SearchBarStories: Meta = { 11 | title: '信息录入/SearchBar 搜索栏', 12 | component: SearchBar, 13 | }; 14 | 15 | export const Basic = () => { 16 | const ref = React.useRef(null); 17 | const onClear = () => { 18 | Toast.show('清除'); 19 | }; 20 | 21 | const onSearch = () => { 22 | Toast.show('搜索'); 23 | }; 24 | 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 49 | 50 | 51 | ); 52 | }; 53 | 54 | Basic.storyName = '基本用法'; 55 | 56 | export default SearchBarStories; 57 | -------------------------------------------------------------------------------- /stories/selector/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import Selector from '@/selector'; 5 | 6 | import DemoWrap from '../../demos/demo-wrap'; 7 | import DemoBlock from '../../demos/demo-block'; 8 | 9 | const SidebarStory: Meta = { 10 | title: '信息录入/Selector 选择组', 11 | component: Selector, 12 | }; 13 | 14 | const options = [ 15 | { 16 | label: '选项一', 17 | value: '1', 18 | }, 19 | { 20 | label: '选项二', 21 | value: '2', 22 | }, 23 | { 24 | label: '选项三', 25 | value: '3', 26 | }, 27 | ]; 28 | 29 | const options1 = [ 30 | { 31 | label: '选项一', 32 | value: '1', 33 | disabled: true, 34 | }, 35 | { 36 | label: '选项二', 37 | value: '2', 38 | }, 39 | { 40 | label: '选项三', 41 | value: '3', 42 | }, 43 | ]; 44 | 45 | const options2 = [ 46 | { 47 | label: '选项一', 48 | value: '1', 49 | description: '描述一', 50 | }, 51 | { 52 | label: '选项二', 53 | value: '2', 54 | description: '描述二', 55 | }, 56 | ]; 57 | 58 | export const Basic = () => { 59 | return ( 60 | 61 | 62 | 63 | 64 | 65 | 66 | { 71 | console.log(value); 72 | console.log(selectorOptions); 73 | }} 74 | /> 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 100 | 101 | 102 | ); 103 | }; 104 | 105 | Basic.storyName = '基本用法'; 106 | 107 | export default SidebarStory; 108 | -------------------------------------------------------------------------------- /stories/sidebar/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import Sidebar from '@/sidebar'; 5 | 6 | import DemoWrap from '../../demos/demo-wrap'; 7 | import DemoBlock from '../../demos/demo-block'; 8 | 9 | const SidebarStory: Meta = { 10 | title: '导航/Sidebar 侧边导航', 11 | component: Sidebar, 12 | }; 13 | 14 | export const Basic = () => { 15 | return ( 16 | 17 | 18 | 19 | 20 | 1 21 | 22 | 23 | 2 24 | 25 | 26 | 3 27 | 28 | 29 | 4 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 1 38 | 39 | 40 | 2 41 | 42 | 43 | 3 44 | 45 | 46 | 4 47 | 48 | 49 | 50 | 51 | ); 52 | }; 53 | 54 | Basic.storyName = '基本用法'; 55 | 56 | export default SidebarStory; 57 | -------------------------------------------------------------------------------- /stories/slider/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import Slider from '@/slider'; 5 | 6 | import DemoWrap from '../../demos/demo-wrap'; 7 | import DemoBlock from '../../demos/demo-block'; 8 | 9 | const SliderStory: Meta = { 10 | title: '信息录入/Slider 滑动条', 11 | component: Slider, 12 | }; 13 | 14 | export const Basic = () => { 15 | const onChange = (value: number) => { 16 | console.log(value); 17 | }; 18 | 19 | const onChangeAfter = (value: number) => { 20 | console.log(value); 21 | }; 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | Basic.storyName = '基本用法'; 47 | 48 | export default SliderStory; 49 | -------------------------------------------------------------------------------- /stories/space/index.scss: -------------------------------------------------------------------------------- 1 | .space-demo-wrap { 2 | font-family: arial; 3 | } 4 | -------------------------------------------------------------------------------- /stories/space/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import Space from '@/space'; 5 | import Button from '@/button'; 6 | 7 | import DemoWrap from '../../demos/demo-wrap'; 8 | import DemoBlock from '../../demos/demo-block'; 9 | 10 | import './index.scss'; 11 | 12 | const LoadingStory: Meta = { 13 | title: '布局/Space 间距', 14 | component: Space, 15 | }; 16 | 17 | export const Basic = () => { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 69 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 81 | 84 | 85 | 86 | 87 | ); 88 | }; 89 | 90 | Basic.storyName = '基本用法'; 91 | 92 | export default LoadingStory; 93 | -------------------------------------------------------------------------------- /stories/spinner-loading/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import SpinnerLoading from '@/spinner-loading'; 5 | import Space from '@/space'; 6 | 7 | import DemoWrap from '../../demos/demo-wrap'; 8 | import DemoBlock from '../../demos/demo-block'; 9 | 10 | const LoadingStory: Meta = { 11 | title: '反馈/Loading 加载中', 12 | component: SpinnerLoading, 13 | }; 14 | 15 | export const Basic = () => { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | Basic.storyName = '基本用法'; 40 | 41 | export default LoadingStory; 42 | -------------------------------------------------------------------------------- /stories/swiper/index.scss: -------------------------------------------------------------------------------- 1 | .swiper-demo { 2 | &-content { 3 | height: 120px; 4 | color: #ffffff; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | font-size: 48px; 9 | user-select: none; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /stories/swiper/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import Swiper, { SwiperRef } from '@/swiper'; 5 | import Button from '@/button'; 6 | import Space from '@/space'; 7 | 8 | import DemoWrap from '../../demos/demo-wrap'; 9 | import DemoBlock from '../../demos/demo-block'; 10 | 11 | import './index.scss'; 12 | 13 | const SwiperStory: Meta = { 14 | title: '信息展示/Swiper 轮播图', 15 | component: Swiper, 16 | subcomponents: { 'Swiper.Item': Swiper.Item }, 17 | }; 18 | 19 | const colors = ['#ace0ff', '#bcffbd', '#e4fabd', '#ffcfac']; 20 | 21 | export const Basic = () => { 22 | const swiperRef = React.useRef(null); 23 | 24 | return ( 25 | 26 | 27 | 28 | {colors.map((color, index) => ( 29 | 30 |
31 | {index + 1} 32 |
33 |
34 | ))} 35 |
36 |
37 | 38 | 39 | 40 | {colors.map((color, index) => ( 41 | 42 |
43 | {index + 1} 44 |
45 |
46 | ))} 47 |
48 |
49 | 50 | 51 | 52 | {colors.map((color, index) => ( 53 | 54 |
55 | {index + 1} 56 |
57 |
58 | ))} 59 |
60 |
61 | 62 | 63 | 64 | {colors.map((color, index) => ( 65 | 66 |
67 | {index + 1} 68 |
69 |
70 | ))} 71 |
72 |
73 | 74 | 75 | 76 | {colors.map((color, index) => ( 77 | 78 |
79 | {index + 1} 80 |
81 |
82 | ))} 83 |
84 | 85 | 92 | 99 | 100 |
101 |
102 | ); 103 | }; 104 | 105 | Basic.storyName = '基本用法'; 106 | 107 | export default SwiperStory; 108 | -------------------------------------------------------------------------------- /stories/tabs/index.scss: -------------------------------------------------------------------------------- 1 | .tabs-demo-list { 2 | border-radius: 15px; 3 | } 4 | 5 | .tabs-demo-active { 6 | border-radius: 15px; 7 | } 8 | -------------------------------------------------------------------------------- /stories/tabs/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import Tabs from '@/tabs'; 5 | 6 | import DemoWrap from '../../demos/demo-wrap'; 7 | import DemoBlock from '../../demos/demo-block'; 8 | 9 | import './index.scss'; 10 | 11 | const TabsStory: Meta = { 12 | title: '导航/ Tabs 标签页', 13 | component: Tabs, 14 | }; 15 | 16 | export const Basic = () => ( 17 | 18 | 19 | 20 | 21 | 内容1 22 | 23 | 24 | 内容2 25 | 26 | 27 | 内容3 28 | 29 | 30 | 31 | 32 | 33 | 40 | 41 | 内容1 42 | 43 | 44 | 内容2 45 | 46 | 47 | 内容3 48 | 49 | 50 | 51 | 52 | ); 53 | 54 | Basic.storyName = '基础用法'; 55 | 56 | export default TabsStory; 57 | -------------------------------------------------------------------------------- /stories/toast/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import Toast from '@/toast'; 5 | import ToastComponent from '@/toast/toast'; 6 | import Button from '@/button'; 7 | 8 | import Space from '@/space'; 9 | 10 | import DemoWrap from '../../demos/demo-wrap'; 11 | import DemoBlock from '../../demos/demo-block'; 12 | 13 | const ToastStory: Meta = { 14 | title: '反馈/ Toast 轻提示', 15 | component: ToastComponent, 16 | }; 17 | 18 | export const Basic = () => ( 19 | 20 | 21 | 33 | 34 | 35 | 36 | 37 | 51 | 52 | 66 | 67 | 81 | 82 | 83 | 84 | ); 85 | 86 | Basic.storyName = '基础用法'; 87 | 88 | export default ToastStory; 89 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@taoyage/configs/shared-tsconfig", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "skipLibCheck": true, 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "jsx": "react", 9 | "outDir": "lib", 10 | "paths": { 11 | "@/*": ["packages/*"] 12 | }, 13 | "rootDir": "." 14 | }, 15 | "include": ["packages", "stories", "demos"], 16 | "exclude": ["node_modules"], 17 | "files": ["./typings.d.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | --------------------------------------------------------------------------------