├── .babelrc.json ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc.js ├── .storybook ├── main.ts ├── preview.ts └── storybook.scss ├── .stylelintrc.json ├── LICENSE ├── README.md ├── css.d.ts ├── form.gif ├── package-lock.json ├── package.json ├── postcss.config.ts ├── src ├── AmountInput │ ├── AmountInput.module.scss │ ├── AmountInput.stories.js │ ├── AmountInputRoot.tsx │ └── index.tsx ├── Calendar │ ├── Calendar.module.scss │ ├── Calendar.stories.js │ ├── CalendarRoot.module.scss │ ├── CalendarRoot.tsx │ └── index.tsx ├── Checkbox │ ├── Checkbox.module.scss │ ├── Checkbox.stories.js │ └── index.tsx ├── CheckboxCards │ ├── CheckboxCards.module.scss │ ├── CheckboxCards.stories.js │ └── index.tsx ├── CheckboxGroup │ ├── CheckboxGroup.module.scss │ ├── CheckboxGroup.stories.js │ └── index.tsx ├── DatePicker │ ├── DatePicker.module.scss │ ├── DatePicker.stories.js │ └── index.tsx ├── DateRangePicker │ ├── DateRangePicker.module.scss │ ├── DateRangePicker.stories.js │ └── index.tsx ├── Form │ ├── Form.module.scss │ ├── Form.stories.js │ ├── ReactHookForm.stories.js │ ├── ReactHookFormWithYup.stories.js │ └── index.tsx ├── Icon │ ├── CheckIcon.tsx │ ├── PieIcon.tsx │ └── index.ts ├── Input │ ├── Input.module.scss │ ├── Input.stories.js │ └── index.tsx ├── MultipleDatePicker │ ├── MultipleDatePicker.module.scss │ ├── MultipleDatePicker.stories.js │ └── index.tsx ├── NumberInput │ ├── NumberInput.module.scss │ ├── NumberInput.stories.js │ └── index.tsx ├── OTPInput │ ├── OTPInput.module.scss │ ├── OTPInput.stories.js │ ├── Slot │ │ ├── Slot.module.scss │ │ └── index.tsx │ └── index.tsx ├── PasswordInput │ ├── PasswordInput.module.scss │ ├── PasswordInput.stories.js │ └── index.tsx ├── PhoneInput │ ├── PhoneInput.module.scss │ ├── PhoneInput.stories.js │ └── index.tsx ├── Radio │ ├── Radio.module.scss │ ├── Radio.stories.js │ └── index.tsx ├── RadioCards │ ├── RadioCards.module.scss │ ├── RadioCards.stories.js │ └── index.tsx ├── RadioGroup │ ├── RadioGroup.module.scss │ ├── RadioGroup.stories.js │ └── index.tsx ├── Select │ ├── Select.module.scss │ ├── Select.stories.js │ └── index.tsx ├── Slider │ ├── Slider.module.scss │ ├── Slider.stories.js │ └── index.tsx ├── Switch │ ├── Switch.module.scss │ ├── Switch.stories.js │ └── index.tsx ├── TextArea │ ├── TextArea.module.scss │ ├── TextArea.stories.js │ └── index.tsx ├── index.ts ├── style │ └── globals.scss └── utils │ ├── cleanValue.ts │ ├── date.ts │ ├── escapeRegExp.ts │ ├── fixedDecimalValue.ts │ ├── formatValue.ts │ ├── getLocaleConfig.ts │ ├── getSuffix.ts │ ├── index.ts │ ├── isNumber.ts │ ├── localStorage.ts │ ├── padTrimValue.ts │ ├── parseAbbrValue.ts │ ├── removeInvalidChars.ts │ ├── removeSeparators.ts │ ├── repositionCursor.ts │ └── useCountDown.ts ├── tsconfig.json ├── tsup.config.ts ├── typings.d.ts └── webpack.config.ts /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "chrome": 100, 9 | "safari": 15, 10 | "firefox": 91 11 | } 12 | } 13 | ], 14 | "@babel/preset-typescript", 15 | "@babel/preset-react" 16 | ], 17 | "plugins": [] 18 | } 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | .idea 38 | package-lock.json 39 | 40 | # storybook static 41 | /storybook-static 42 | 43 | # dist 44 | /dist 45 | 46 | /AmountInput 47 | /Calendar 48 | /Checkbox 49 | /CheckboxCards 50 | /CheckboxGroup 51 | /DatePicker 52 | /DateRangePicker 53 | /Form 54 | /Input 55 | /MultipleDatePicker 56 | /NumberInput 57 | /OTPInput 58 | /PasswordInput 59 | /PhoneInput 60 | /Radio 61 | /RadioCards 62 | /RadioGroup 63 | /Select 64 | /Slider 65 | /Switch 66 | /TextArea 67 | 68 | /index.js 69 | /index.css 70 | /styles.css 71 | /index.mjs -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "browser": true, 6 | "es2024": true 7 | }, 8 | "parser": "@typescript-eslint/parser", 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/strict", 12 | "plugin:@typescript-eslint/stylistic", 13 | "plugin:css-modules/recommended", 14 | "plugin:react/recommended", 15 | "plugin:compat/recommended", 16 | "plugin:prettier/recommended", 17 | "prettier", 18 | "plugin:storybook/recommended", 19 | "plugin:storybook/recommended" 20 | ], 21 | "plugins": ["@typescript-eslint", "compat", "prettier", "css-modules"], 22 | "parserOptions": { 23 | "ecmaFeatures": { 24 | "jsx": true 25 | }, 26 | "ecmaVersion": 12, 27 | "sourceType": "module" 28 | }, 29 | "settings": { 30 | "react": { 31 | "version": "detect" 32 | } 33 | }, 34 | "rules": { 35 | "jsx-a11y/alt-text": ["off"], 36 | "linebreak-style": [2, "unix"], 37 | "semi": [2, "always"], 38 | "no-multiple-empty-lines": [ 39 | 2, 40 | { 41 | "max": 1 42 | } 43 | ], 44 | "no-useless-catch": 0, 45 | "react/react-in-jsx-scope": 0, 46 | "react/prop-types": 0, 47 | "@typescript-eslint/no-explicit-any": 0, 48 | "@typescript-eslint/no-non-null-assertion": 0, 49 | "@typescript-eslint/no-non-null-asserted-optional-chain": 0 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | .idea 38 | 39 | # storybook static 40 | /storybook-static 41 | 42 | # dist 43 | /dist 44 | 45 | /AmountInput 46 | /Calendar 47 | /Checkbox 48 | /CheckboxCards 49 | /CheckboxGroup 50 | /DatePicker 51 | /DateRangePicker 52 | /Form 53 | /Input 54 | /MultipleDatePicker 55 | /NumberInput 56 | /OTPInput 57 | /PasswordInput 58 | /PhoneInput 59 | /Radio 60 | /RadioCards 61 | /RadioGroup 62 | /Select 63 | /Slider 64 | /Switch 65 | /TextArea 66 | 67 | /index.js 68 | /index.css 69 | /styles.css 70 | /index.mjs 71 | /index.d.mts 72 | /index.d.ts 73 | *storybook.log -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run stylelint || (false;) 5 | npm run lint || (false;) 6 | npm run format || (false;) 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .husky 4 | coverage 5 | .prettierignore 6 | .stylelintignore 7 | .eslintignore -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [], 3 | trailingComma: 'es5', 4 | tabWidth: 2, 5 | semi: true, 6 | singleQuote: true, 7 | }; 8 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-webpack5'; 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 5 | addons: [ 6 | { 7 | name: '@storybook/addon-styling-webpack', 8 | options: { 9 | rules: [ 10 | { 11 | test: /\.css$/i, 12 | use: ['style-loader', 'css-loader'], 13 | }, 14 | { 15 | test: /\.module\.scss$/i, 16 | use: [ 17 | 'style-loader', 18 | { 19 | loader: 'css-loader', 20 | options: { 21 | modules: { 22 | mode: 'local', 23 | localIdentName: '[local]_[hash:base64:5]', 24 | }, 25 | importLoaders: 1, 26 | esModule: false, 27 | }, 28 | }, 29 | 'sass-loader', 30 | ], 31 | }, 32 | { 33 | test: /(storybook|globals)\.scss$/i, 34 | use: ['style-loader', 'css-loader', 'sass-loader'], 35 | }, 36 | ], 37 | }, 38 | }, 39 | '@storybook/addon-webpack5-compiler-swc', 40 | '@storybook/addon-onboarding', 41 | '@storybook/addon-links', 42 | '@storybook/addon-essentials', 43 | '@storybook/addon-interactions', 44 | 'storybook-dark-mode', 45 | ], 46 | framework: { 47 | name: '@storybook/react-webpack5', 48 | options: {}, 49 | }, 50 | docs: {}, 51 | typescript: { 52 | reactDocgen: 'react-docgen-typescript', 53 | }, 54 | }; 55 | 56 | export default config; 57 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | 3 | import 'react-day-picker/src/style.css'; 4 | import 'rc-slider/assets/index.css'; 5 | import '../src/style/globals.scss'; 6 | import './storybook.scss'; 7 | 8 | const preview: Preview = { 9 | parameters: { 10 | controls: { 11 | matchers: { 12 | color: /(background|color)$/i, 13 | date: /Date$/i, 14 | }, 15 | }, 16 | }, 17 | }; 18 | 19 | export default preview; 20 | -------------------------------------------------------------------------------- /.storybook/storybook.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: 6 | GalanoGrotesque, 7 | 'Noto Sans', 8 | -apple-system, 9 | BlinkMacSystemFont, 10 | 'Segoe UI', 11 | Roboto, 12 | Oxygen, 13 | Ubuntu, 14 | Cantarell, 15 | 'Fira Sans', 16 | 'Droid Sans', 17 | 'Helvetica Neue', 18 | sans-serif; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | background: var(--rfe-white); 22 | } 23 | 24 | h2 { 25 | color: var(--rfe-color); 26 | } 27 | 28 | .docs-story { 29 | background: var(--rfe-white); 30 | } 31 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard-scss", 4 | "stylelint-config-prettier-scss" 5 | ], 6 | "plugins": [ 7 | "stylelint-declaration-strict-value", 8 | "stylelint-no-unsupported-browser-features" 9 | ], 10 | "rules": { 11 | "selector-class-pattern": "^[a-z][a-zA-Z0-9]+$", 12 | "scss/no-global-function-names": null, 13 | "no-descending-specificity": null, 14 | "media-feature-range-notation": null, 15 | "scale-unlimited/declaration-strict-value": [ 16 | ["/color$/"], 17 | { 18 | "disableFix": true 19 | } 20 | ], 21 | "declaration-block-no-redundant-longhand-properties": [ 22 | true, 23 | { 24 | "ignoreShorthands": ["/flex/"] 25 | } 26 | ], 27 | "plugin/no-unsupported-browser-features": [ 28 | true, 29 | { 30 | "browsers": [">1%", "not dead", "not op_mini all"], 31 | "ignorePartialSupport": true, 32 | "ignore": [ 33 | "css-nesting", 34 | "css-resize", 35 | "css-focus-visible", 36 | "css3-cursors" 37 | ] 38 | } 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 STARTBASE LTD 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

@start-base/react-form-elements

8 | 9 |

10 | Unlock a robust collection of form-focused React components, thoughtfully crafted for maximum ease of use and flexibility. Built with Next.js compatibility, these components streamline the website development process, enhancing efficiency and empowering you to create custom, user-friendly forms effortlessly. 11 |

12 | 13 |

14 | license 15 | weekly 16 | size 17 | npm 18 |

19 | 20 | --- 21 | 22 | ![](form.gif) 23 | 24 | ## Documentation 25 | 26 | For full documentation, please visit [start-ui.startbase.dev](https://start-ui.startbase.dev/docs). 27 | 28 | ## Installation 29 | 30 | To install, you can use [npm](https://npmjs.org) or [yarn](https://yarnpkg.com): 31 | 32 | ```bash title='Terminal' 33 | $ npm install @start-base/react-form-elements 34 | # or 35 | $ yarn add @start-base/react-form-elements 36 | ``` 37 | 38 | Make sure to import `styles.css` to your app root: 39 | 40 | ```tsx 41 | import '@start-base/react-form-elements/styles.css'; 42 | ``` 43 | 44 | ## Features 45 | 46 | - CSS variables for theming. 47 | - Classnames for fine tuning. 48 | - Built-in dark mode support. 49 | - Variants for different styles. 50 | 51 | ## Styling 52 | 53 | ### With CSS Variables 54 | 55 | You can use CSS variables to customize the look and feel of the components. Here's a list of all available variables: 56 | 57 | ```css title="global.css" 58 | --rfe-transparent: transparent; 59 | --rfe-white: #fff; 60 | --rfe-white-rgb: 255 255 255; 61 | --rfe-black: #000; 62 | --rfe-black-rgb: 0 0 0; 63 | --rfe-color: #000; 64 | --rfe-color-placeholder: #646464; 65 | --rfe-color-error: #ff383e; 66 | --rfe-border: #e6e6e6; 67 | --rfe-background: #fafafa; 68 | --rfe-background-selected: #f0f0f0; 69 | --rfe-background-disabled: #e1e1e1; 70 | --rfe-border-radius: 6px; 71 | --rfe-font-size: 16px; 72 | --rfe-placeholder-font-size: 14px; 73 | --rfe-focused-font-size: 12px; 74 | --rfe-error-font-size: 12px; 75 | --rfe-input-height: 56px; 76 | --rfe-spacing: 16px; 77 | --rfe-label-spacing: 10px; 78 | --rfe-focus: 0 0 10px #dcdcdc; 79 | --rfe-font-family: 'Arial', sans-serif; 80 | ``` 81 | 82 | ### With Classnames 83 | 84 | You can use classnames to customize the look and feel of the components. Here's an example of how to use classnames: 85 | 86 | ```css title="CustomInput.module.scss" 87 | .input { 88 | &:focus { 89 | } 90 | } 91 | 92 | .label { 93 | } 94 | ``` 95 | 96 | ```jsx title="CustomInput.js" 97 | import { forwardRef } from 'react'; 98 | 99 | import Input from '@start-base/react-form-elements/Input'; 100 | 101 | import styles from './CustomInput.module.scss'; 102 | 103 | const CustomInput = forwardRef((props, ref) => ( 104 | 111 | )); 112 | 113 | export default CustomInput; 114 | ``` 115 | 116 | Or for all form elements you can use single css file. 117 | 118 | ```css title="FormElements.module.scss" 119 | .input { 120 | &:focus { 121 | } 122 | } 123 | 124 | .inputFocused { 125 | } 126 | ``` 127 | 128 | ## Demos 129 | 130 | For live demos, please visit our [Storybook](https://react-form-elements-eosin.vercel.app/) page. 131 | 132 | ## Contributing 133 | 134 | Contributions are welcomed. Feel free to submit pull requests and improvements to the project! 135 | 136 | 137 | 138 | 144 | 150 | 151 |
139 |
140 | 141 | yunusozcan 142 | 143 |
145 |
146 | 147 | emreonursoy 148 | 149 |
152 | -------------------------------------------------------------------------------- /css.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'csstype' { 2 | type Properties = Record<`--${string}`, string>; 3 | } 4 | -------------------------------------------------------------------------------- /form.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/startbase-dev/react-form-elements/55ea969dedd1cd2f956d2679287840d064b13dd9/form.gif -------------------------------------------------------------------------------- /postcss.config.ts: -------------------------------------------------------------------------------- 1 | import autoprefixer from 'autoprefixer'; 2 | 3 | export default { 4 | plugins: [autoprefixer()], 5 | }; 6 | -------------------------------------------------------------------------------- /src/AmountInput/AmountInput.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-bottom: var(--rfe-spacing); 3 | } 4 | 5 | .inputRoot { 6 | position: relative; 7 | width: 100%; 8 | } 9 | 10 | .label { 11 | font-size: var(--rfe-font-size); 12 | line-height: 1; 13 | color: var(--rfe-color); 14 | margin-bottom: 0; 15 | position: absolute; 16 | top: 0; 17 | left: 16px; 18 | transform: translateY(21px); 19 | transition: 20 | transform 250ms, 21 | font-size 250ms; 22 | pointer-events: all; 23 | cursor: text; 24 | 25 | &.disableShrink { 26 | display: block; 27 | position: static; 28 | margin-bottom: 10px; 29 | color: var(--rfe-color); 30 | transform: none; 31 | } 32 | } 33 | 34 | .focusedLabel { 35 | font-size: var(--rfe-focused-font-size); 36 | transform: translateY(9px); 37 | } 38 | 39 | .disabledLabel { 40 | pointer-events: none; 41 | cursor: not-allowed; 42 | } 43 | 44 | .input { 45 | width: 100%; 46 | line-height: 24px; 47 | color: var(--rfe-color); 48 | padding: 20px 16px 0; 49 | background: var(--rfe-background); 50 | border: 1px solid var(--rfe-border); 51 | border-radius: var(--rfe-border-radius); 52 | height: var(--rfe-input-height); 53 | transition: all 250ms; 54 | 55 | &:focus { 56 | box-shadow: var(--rfe-focus); 57 | 58 | &::placeholder { 59 | color: var(--rfe-color-placeholder); 60 | } 61 | } 62 | 63 | &::placeholder { 64 | color: var(--rfe-color-placeholder); 65 | font-size: var(--rfe-placeholder-font-size); 66 | } 67 | 68 | &:-webkit-autofill, 69 | &:focus, 70 | &:not([value='']) { 71 | &:not(.disableShrink) { 72 | & ~ .label { 73 | font-size: var(--rfe-focused-font-size); 74 | transform: translateY(9px); 75 | } 76 | } 77 | } 78 | 79 | &.disableShrink { 80 | padding: 16px; 81 | } 82 | } 83 | 84 | .append, 85 | .prepend { 86 | position: absolute; 87 | color: var(--rfe-color); 88 | top: 50%; 89 | transform: translateY(-50%); 90 | display: flex; 91 | align-items: center; 92 | justify-content: center; 93 | pointer-events: none; 94 | } 95 | 96 | .prepend { 97 | left: 16px; 98 | font-size: var(--rfe-placeholder-font-size); 99 | line-height: 16px; 100 | padding-right: 13px; 101 | border-right: 1px solid var(--rfe-border); 102 | height: var(--rfe-input-height); 103 | 104 | & ~ .label { 105 | left: 62px; 106 | } 107 | 108 | & ~ .input { 109 | padding: 20px 16px 0 60px; 110 | 111 | &.disableShrink { 112 | padding: 16px 16px 16px 60px; 113 | } 114 | } 115 | } 116 | 117 | .append { 118 | right: 16px; 119 | height: 56px; 120 | 121 | & ~ .input { 122 | padding: 20px 50px 0 16px; 123 | 124 | &.disableShrink { 125 | padding: 0 50px 0 16px; 126 | } 127 | } 128 | } 129 | 130 | .appendDisabledShrink { 131 | top: 26px; 132 | transform: none; 133 | } 134 | 135 | .prependDisabledShrink { 136 | top: 26px; 137 | transform: none; 138 | } 139 | 140 | .noLabel { 141 | padding: 16px; 142 | } 143 | 144 | .errorLabel { 145 | color: var(--rfe-color-error); 146 | font-size: var(--rfe-error-font-size); 147 | margin-top: var(--rfe-label-spacing); 148 | pointer-events: none; 149 | } 150 | 151 | .inputError { 152 | border-color: var(--rfe-color-error) !important; 153 | } 154 | 155 | .disabled { 156 | background: var(--rfe-background-disabled); 157 | color: var(--rfe-color); 158 | cursor: not-allowed; 159 | opacity: 1; 160 | } 161 | -------------------------------------------------------------------------------- /src/AmountInput/AmountInput.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { PieIcon } from '../Icon'; 4 | import AmountInput from './index'; 5 | 6 | const Template = (arg) => { 7 | const [inputs, setInputs] = useState({}); 8 | const onChange = (e) => { 9 | const { name, value, type } = e.target; 10 | 11 | setInputs((prevState) => ({ 12 | ...prevState, 13 | [name]: type === 'checkbox' ? !prevState[name] : value, 14 | })); 15 | }; 16 | 17 | return ( 18 | <> 19 |

{arg.title}

20 | 26 | 27 | ); 28 | }; 29 | export const AmountInputComponent = Template.bind({}); 30 | AmountInputComponent.args = { title: 'Input' }; 31 | 32 | export const AmountInputWithLabel = Template.bind({}); 33 | AmountInputWithLabel.args = { title: 'Input with label', label: 'Label' }; 34 | 35 | export const AmountInputWithPlaceholder = Template.bind({}); 36 | AmountInputWithPlaceholder.args = { 37 | title: 'AmountInput with placeholder', 38 | placeholder: 'Placeholder', 39 | }; 40 | 41 | export const AmountInputWithPlaceholderAndLabel = Template.bind({}); 42 | AmountInputWithPlaceholderAndLabel.args = { 43 | title: 'AmountInput with placeholder and label', 44 | placeholder: 'Placeholder', 45 | label: 'Label', 46 | }; 47 | 48 | export const AmountInputWithPlaceholderAndLabelAndDisableShrink = Template.bind( 49 | {} 50 | ); 51 | AmountInputWithPlaceholderAndLabelAndDisableShrink.args = { 52 | title: 'AmountInput with placeholder and label and disable shrink', 53 | placeholder: 'Placeholder', 54 | label: 'Label', 55 | disableShrink: true, 56 | }; 57 | 58 | export const AmountInputDisabled = Template.bind({}); 59 | AmountInputDisabled.args = { 60 | title: 'Disabled AmountInput', 61 | placeholder: 'Placeholder', 62 | disabled: true, 63 | label: 'Label', 64 | }; 65 | 66 | export const AmountInputAppend = Template.bind({}); 67 | AmountInputAppend.args = { 68 | title: 'AmountInput with append', 69 | placeholder: 'Placeholder', 70 | label: 'Label', 71 | append: , 72 | }; 73 | 74 | export const AmountInputPrepend = Template.bind({}); 75 | AmountInputPrepend.args = { 76 | title: 'Input with prepend', 77 | placeholder: 'Placeholder', 78 | label: 'Label', 79 | prepend: , 80 | }; 81 | 82 | export const AmountInputError = Template.bind({}); 83 | AmountInputError.args = { 84 | title: 'AmountInput with error', 85 | label: 'Label', 86 | error: 'Error Message', 87 | }; 88 | 89 | const Component = { 90 | title: 'Form/AmountInput', 91 | component: AmountInput, 92 | }; 93 | 94 | export default Component; 95 | -------------------------------------------------------------------------------- /src/AmountInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useCallback, useMemo } from 'react'; 2 | import cx from 'clsx'; 3 | import AmountInputRoot, { type AmountInputRootProps } from './AmountInputRoot'; 4 | import s from './AmountInput.module.scss'; 5 | import { FieldError } from 'react-hook-form'; 6 | 7 | interface AmountInputProps extends AmountInputRootProps { 8 | name: string; 9 | error?: 10 | | boolean 11 | | string 12 | | { message?: string } 13 | | null 14 | | undefined 15 | | FieldError; 16 | label?: string | null; 17 | value?: string | number | readonly string[] | undefined; 18 | inputClassName?: string | null; 19 | labelClassName?: string | null; 20 | errorClassName?: string | null; 21 | prepend?: React.ReactNode | null; 22 | prependClassName?: string | null; 23 | append?: React.ReactNode | null; 24 | appendClassName?: string | null; 25 | disableShrink?: boolean; 26 | disabled?: boolean; 27 | } 28 | 29 | const AmountInput = forwardRef( 30 | ( 31 | { 32 | name, 33 | onChange, 34 | error = null, 35 | label = null, 36 | placeholder = undefined, 37 | value = undefined, 38 | inputClassName = null, 39 | labelClassName = null, 40 | errorClassName = null, 41 | prepend = null, 42 | prependClassName = null, 43 | append = null, 44 | appendClassName = null, 45 | disableShrink = false, 46 | disabled = false, 47 | ...rest 48 | }, 49 | inputRef 50 | ) => { 51 | const handleChange = useCallback( 52 | (value: string, name: string) => { 53 | onChange!({ 54 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 55 | // @ts-ignore 56 | target: { 57 | name, 58 | value, 59 | }, 60 | }); 61 | }, 62 | [onChange] 63 | ); 64 | 65 | const input = useMemo(() => { 66 | return ( 67 | 87 | ); 88 | }, [ 89 | name, 90 | disableShrink, 91 | label, 92 | disabled, 93 | error, 94 | inputClassName, 95 | placeholder, 96 | value, 97 | inputRef, 98 | handleChange, 99 | rest, 100 | ]); 101 | 102 | const labelEl = useMemo(() => { 103 | return ( 104 | 115 | ); 116 | }, [disabled, name, disableShrink, label, placeholder, labelClassName]); 117 | 118 | const errorMessage = useMemo(() => { 119 | let message: string | null = null; 120 | if (typeof error === 'string') { 121 | message = error; 122 | } else if (error && typeof error === 'object' && error.message) { 123 | message = error.message; 124 | } 125 | return message; 126 | }, [error]); 127 | 128 | return ( 129 |
130 |
131 | {prepend && ( 132 |
138 | {prepend} 139 |
140 | )} 141 | 142 | {append && ( 143 |
149 | {append} 150 |
151 | )} 152 | 153 | {label && disableShrink ? labelEl : null} 154 | 155 | {input} 156 | 157 | {label && !disableShrink ? labelEl : null} 158 |
159 | {errorMessage ? ( 160 |
165 | {errorMessage} 166 |
167 | ) : null} 168 |
169 | ); 170 | } 171 | ); 172 | 173 | AmountInput.displayName = 'AmountInput'; 174 | 175 | export default AmountInput; 176 | 177 | export { type AmountInputProps }; 178 | -------------------------------------------------------------------------------- /src/Calendar/Calendar.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-bottom: var(--rfe-spacing); 3 | } 4 | 5 | .inputRoot { 6 | position: relative; 7 | } 8 | 9 | .label { 10 | font-size: var(--rfe-font-size); 11 | line-height: 1; 12 | color: var(--rfe-color); 13 | margin-bottom: var(--rfe-label-spacing); 14 | transition: all 250ms; 15 | display: block; 16 | } 17 | 18 | .errorLabel { 19 | color: var(--rfe-color-error); 20 | font-size: var(--rfe-error-font-size); 21 | margin-top: var(--rfe-label-spacing); 22 | pointer-events: none; 23 | } 24 | -------------------------------------------------------------------------------- /src/Calendar/Calendar.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Calendar from './index'; 4 | 5 | const Template = (arg) => { 6 | const [inputs, setInputs] = useState({}); 7 | const onChange = (e) => { 8 | const { name, value, type } = e.target; 9 | 10 | setInputs((prevState) => ({ 11 | ...prevState, 12 | [name]: type === 'checkbox' ? !prevState[name] : value, 13 | })); 14 | }; 15 | 16 | return ( 17 | <> 18 |

{arg.title}

19 | 25 | 26 | ); 27 | }; 28 | export const CalendarComponent = Template.bind({}); 29 | CalendarComponent.args = { title: 'Calendar' }; 30 | 31 | const Component = { 32 | title: 'Form/Calendar', 33 | component: Calendar, 34 | }; 35 | 36 | export default Component; 37 | -------------------------------------------------------------------------------- /src/Calendar/CalendarRoot.module.scss: -------------------------------------------------------------------------------- 1 | .calendar { 2 | margin: 0 !important; 3 | } 4 | 5 | .months { 6 | background: var(--rfe-background) !important; 7 | color: var(--rfe-color) !important; 8 | border-radius: var(--rfe-border-radius) !important; 9 | padding: 1rem !important; 10 | border: 1px solid var(--rfe-border); 11 | transition: all 250ms; 12 | display: flex; 13 | gap: var(--rfe-spacing); 14 | width: fit-content; 15 | 16 | > div { 17 | margin: 0; 18 | } 19 | 20 | @media (max-width: 576px) { 21 | flex-direction: column; 22 | } 23 | } 24 | 25 | .month { 26 | background: var(--rfe-background) !important; 27 | color: var(--rfe-color) !important; 28 | border-radius: var(--rfe-border-radius) !important; 29 | padding: 1rem !important; 30 | border: 1px solid var(--rfe-border); 31 | transition: all 250ms; 32 | display: block; 33 | } 34 | 35 | .captionLabel { 36 | z-index: unset; 37 | } 38 | 39 | .disabled { 40 | background: var(--rfe-background-disabled) !important; 41 | color: var(--rfe-color); 42 | cursor: not-allowed; 43 | } 44 | 45 | .error { 46 | border-color: var(--rfe-color-error) !important; 47 | } 48 | -------------------------------------------------------------------------------- /src/Calendar/CalendarRoot.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | DayPicker, 4 | DayPickerMultipleProps, 5 | type DayPickerRangeProps, 6 | type DayPickerSingleProps, 7 | } from 'react-day-picker'; 8 | import cx from 'clsx'; 9 | import s from './CalendarRoot.module.scss'; 10 | 11 | export interface CalendarRootRangeProps extends DayPickerRangeProps { 12 | numberOfMonths?: number; 13 | classNames?: Partial>; 14 | calendarClassName?: string | null; 15 | className?: string; 16 | disabled?: boolean; 17 | error?: boolean | string | { message?: string } | null; 18 | } 19 | 20 | export interface CalendarRootSingleProps extends DayPickerSingleProps { 21 | numberOfMonths?: number; 22 | classNames?: Partial>; 23 | calendarClassName?: string | null; 24 | className?: string; 25 | disabled?: boolean; 26 | error?: boolean | string | { message?: string } | null; 27 | } 28 | 29 | export interface CalendarRootMultipleProps extends DayPickerMultipleProps { 30 | numberOfMonths?: number; 31 | classNames?: Partial>; 32 | calendarClassName?: string | null; 33 | className?: string; 34 | disabled?: boolean; 35 | error?: boolean | string | { message?: string } | null; 36 | } 37 | 38 | const CalendarRoot: React.FC< 39 | CalendarRootRangeProps | CalendarRootSingleProps | CalendarRootMultipleProps 40 | > = ({ 41 | disabled, 42 | error, 43 | className, 44 | classNames, 45 | calendarClassName, 46 | numberOfMonths = 1, 47 | ...rest 48 | }) => { 49 | return ( 50 | 1 74 | ? { 75 | months: cx({ 76 | [s.error]: error, 77 | [s.months]: numberOfMonths > 1, 78 | [s.disabled]: disabled, 79 | [classNames?.months || '']: classNames?.months, 80 | [calendarClassName || '']: calendarClassName, 81 | }), 82 | } 83 | : {}), 84 | }} 85 | /> 86 | ); 87 | }; 88 | 89 | export default CalendarRoot; 90 | -------------------------------------------------------------------------------- /src/Calendar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useMemo } from 'react'; 2 | import cx from 'clsx'; 3 | import CalendarRoot, { CalendarRootSingleProps } from './CalendarRoot'; 4 | import s from './Calendar.module.scss'; 5 | import { FieldError } from 'react-hook-form'; 6 | 7 | interface CalendarProps extends CalendarRootSingleProps { 8 | name: string; 9 | onChange: (event: React.ChangeEvent) => void; 10 | error?: 11 | | boolean 12 | | string 13 | | { message?: string } 14 | | null 15 | | undefined 16 | | FieldError; 17 | label?: string; 18 | value?: Date | null; 19 | labelClassName?: string; 20 | errorClassName?: string; 21 | calendarClassName?: string; 22 | disabled?: boolean; 23 | } 24 | 25 | const Calendar = forwardRef( 26 | ( 27 | { 28 | name, 29 | onChange, 30 | error = null, 31 | label = null, 32 | value = null, 33 | labelClassName = null, 34 | errorClassName = null, 35 | calendarClassName = null, 36 | disabled = false, 37 | ...rest 38 | }, 39 | inputRef 40 | ) => { 41 | const handleDaySelect = (date: Date | undefined) => { 42 | if (date) { 43 | onChange({ 44 | target: { 45 | name: name, 46 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 47 | // @ts-ignore 48 | value: date, 49 | }, 50 | }); 51 | } 52 | }; 53 | 54 | const errorMessage = useMemo(() => { 55 | if (error && typeof error === 'string') { 56 | return error; 57 | } 58 | if (error && typeof error === 'object' && error?.message) { 59 | return error.message; 60 | } 61 | return null; 62 | }, [error]); 63 | 64 | const labelEl = useMemo( 65 | () => ( 66 | 74 | ), 75 | [name, labelClassName, label] 76 | ); 77 | 78 | return ( 79 |
80 |
81 | {label ? labelEl : null} 82 | 83 | 89 | 90 | 99 |
100 | {errorMessage && ( 101 | 106 | {errorMessage} 107 | 108 | )} 109 |
110 | ); 111 | } 112 | ); 113 | 114 | Calendar.displayName = 'Calendar'; 115 | 116 | export default Calendar; 117 | 118 | export { type CalendarProps }; 119 | -------------------------------------------------------------------------------- /src/Checkbox/Checkbox.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | flex-direction: column; 4 | margin-bottom: var(--rfe-spacing); 5 | display: inline-flex; 6 | } 7 | 8 | .inputRoot { 9 | position: relative; 10 | display: flex; 11 | align-items: center; 12 | justify-content: flex-start; 13 | } 14 | 15 | .label { 16 | margin-top: 1px; 17 | font-size: var(--rfe-font-size); 18 | line-height: 20px; 19 | color: var(--rfe-color); 20 | pointer-events: all; 21 | cursor: pointer; 22 | } 23 | 24 | .box { 25 | width: 28px; 26 | height: 28px; 27 | border: solid 1px var(--rfe-border); 28 | border-radius: var(--rfe-border-radius); 29 | margin-right: 16px; 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | color: var(--rfe-white); 34 | flex-shrink: 0; 35 | background-color: var(--rfe-background); 36 | transition: all 250ms; 37 | 38 | &:hover { 39 | box-shadow: var(--rfe-focus); 40 | } 41 | } 42 | 43 | .noLabel { 44 | margin: 0; 45 | } 46 | 47 | .inputError { 48 | border-color: var(--rfe-color-error) !important; 49 | } 50 | 51 | .boxDisabled { 52 | background-color: var(--rfe-background-disabled); 53 | 54 | &:hover { 55 | box-shadow: none; 56 | cursor: not-allowed; 57 | } 58 | } 59 | 60 | .icon { 61 | display: none; 62 | line-height: 1; 63 | color: var(--rfe-color); 64 | } 65 | 66 | .input { 67 | position: fixed; 68 | top: 0; 69 | left: 0; 70 | z-index: 0; 71 | opacity: 0; 72 | width: 0; 73 | height: 0; 74 | } 75 | 76 | .input:checked ~ .box { 77 | background-color: var(--rfe-background-selected); 78 | border-color: var(--rfe-border); 79 | } 80 | 81 | .input:checked ~ .boxDisabled { 82 | background: var(--rfe-background-disabled) !important; 83 | } 84 | 85 | .input:checked ~ .box .icon { 86 | display: block; 87 | } 88 | 89 | .input:focus-visible ~ .box { 90 | box-shadow: var(--rfe-focus); 91 | } 92 | 93 | .errorLabel { 94 | color: var(--rfe-color-error); 95 | font-size: var(--rfe-error-font-size); 96 | margin-top: var(--rfe-label-spacing); 97 | pointer-events: none; 98 | } 99 | -------------------------------------------------------------------------------- /src/Checkbox/Checkbox.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Checkbox from './index'; 4 | 5 | const Template = (args) => { 6 | const [inputs, setInputs] = useState({}); 7 | const onChange = (e) => { 8 | const { name, value, type } = e.target; 9 | 10 | setInputs((prevState) => ({ 11 | ...prevState, 12 | [name]: type === 'checkbox' ? !prevState[name] : value, 13 | })); 14 | }; 15 | 16 | return ( 17 | <> 18 |

{args.title}

19 |
26 | 33 |
34 | 35 | ); 36 | }; 37 | 38 | export const CheckboxComponent = Template.bind({}); 39 | CheckboxComponent.args = { title: 'CheckBox' }; 40 | 41 | export const CheckboxComponentChecked = Template.bind({}); 42 | CheckboxComponentChecked.args = { title: 'CheckBox', checked: true }; 43 | 44 | export const CheckboxComponentDisabled = Template.bind({}); 45 | CheckboxComponentDisabled.args = { title: 'CheckBox', disabled: true }; 46 | 47 | export const CheckboxComponentError = Template.bind({}); 48 | CheckboxComponentError.args = { 49 | title: 'CheckBox with error', 50 | error: 'Error Message', 51 | }; 52 | 53 | const Component = { 54 | title: 'Form/Checkbox', 55 | component: CheckboxComponent, 56 | }; 57 | 58 | export default Component; 59 | -------------------------------------------------------------------------------- /src/Checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useMemo, ChangeEvent, ReactNode } from 'react'; 2 | import cx from 'clsx'; 3 | import { CheckIcon } from '../Icon'; 4 | import s from './Checkbox.module.scss'; 5 | import { FieldError } from 'react-hook-form'; 6 | 7 | interface CheckboxProps extends React.InputHTMLAttributes { 8 | name: string; 9 | onChange: (e: ChangeEvent) => void; 10 | label?: ReactNode; 11 | checked?: boolean; 12 | inputClassName?: string; 13 | labelClassName?: string; 14 | errorClassName?: string; 15 | disabled?: boolean; 16 | error?: 17 | | boolean 18 | | string 19 | | { message?: string } 20 | | null 21 | | undefined 22 | | FieldError; 23 | } 24 | 25 | const Checkbox = forwardRef( 26 | ( 27 | { 28 | name, 29 | onChange, 30 | label = null, 31 | checked = false, 32 | inputClassName = null, 33 | labelClassName = null, 34 | errorClassName = null, 35 | disabled = false, 36 | error = null, 37 | }, 38 | inputRef 39 | ) => { 40 | const errorMessage = useMemo(() => { 41 | if (error && typeof error === 'string') { 42 | return error; 43 | } else if (error && typeof error === 'object' && error?.message) { 44 | return error.message; 45 | } 46 | return null; 47 | }, [error]); 48 | 49 | return ( 50 | 90 | ); 91 | } 92 | ); 93 | 94 | Checkbox.displayName = 'Checkbox'; 95 | 96 | export default Checkbox; 97 | 98 | export { type CheckboxProps }; 99 | -------------------------------------------------------------------------------- /src/CheckboxCards/CheckboxCards.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | pointer-events: all; 3 | position: relative; 4 | display: flex; 5 | flex-direction: column; 6 | margin-bottom: var(--rfe-spacing); 7 | 8 | & label { 9 | margin-bottom: var(--rfe-label-spacing); 10 | } 11 | 12 | & label:last-of-type { 13 | margin-bottom: 0; 14 | } 15 | } 16 | 17 | .label { 18 | font-size: var(--rfe-font-size); 19 | line-height: 1; 20 | color: var(--rfe-color); 21 | margin-bottom: var(--rfe-label-spacing); 22 | } 23 | 24 | .errorLabel { 25 | color: var(--rfe-color-error); 26 | font-size: var(--rfe-error-font-size); 27 | margin-top: var(--rfe-label-spacing); 28 | } 29 | 30 | .container { 31 | display: flex; 32 | flex-wrap: wrap; 33 | gap: var(--rfe-spacing); 34 | } 35 | 36 | .row { 37 | flex-direction: row; 38 | } 39 | 40 | .column { 41 | flex-direction: column; 42 | } 43 | 44 | .card { 45 | cursor: pointer; 46 | border: solid 1px var(--rfe-border); 47 | border-radius: var(--rfe-border-radius); 48 | display: flex; 49 | padding: 0; 50 | align-items: center; 51 | justify-content: center; 52 | color: var(--rfe-white); 53 | flex-shrink: 0; 54 | background-color: var(--rfe-background); 55 | transition: all 250ms; 56 | 57 | label { 58 | width: 100%; 59 | text-align: center; 60 | padding: var(--rfe-label-spacing); 61 | 62 | div { 63 | width: 100%; 64 | text-align: center; 65 | } 66 | } 67 | } 68 | 69 | .card:hover { 70 | box-shadow: var(--rfe-focus); 71 | } 72 | 73 | .card:focus-within { 74 | box-shadow: var(--rfe-focus); 75 | } 76 | 77 | .selected { 78 | background-color: var(--rfe-background-selected); 79 | border-color: var(--rfe-border); 80 | } 81 | 82 | .disabled { 83 | background-color: var(--rfe-background-disabled); 84 | box-shadow: none !important; 85 | 86 | & > label:hover { 87 | box-shadow: none; 88 | cursor: not-allowed; 89 | 90 | div { 91 | cursor: not-allowed; 92 | } 93 | } 94 | } 95 | 96 | .hide { 97 | display: none; 98 | } 99 | 100 | .inputError { 101 | border-color: var(--rfe-color-error) !important; 102 | } 103 | -------------------------------------------------------------------------------- /src/CheckboxCards/CheckboxCards.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Index from './index'; 4 | 5 | const Template = (args) => { 6 | const [inputs, setInputs] = useState({}); 7 | const onChange = (e) => { 8 | const { name, value } = e.target; 9 | 10 | setInputs((prevState) => ({ 11 | ...prevState, 12 | [name]: value, 13 | })); 14 | }; 15 | 16 | return ( 17 | <> 18 |

{args.title}

19 |
26 | 37 |
38 | 39 | ); 40 | }; 41 | 42 | export const CheckboxCardsComponent = Template.bind({}); 43 | CheckboxCardsComponent.args = { title: 'CheckboxCards' }; 44 | 45 | export const CheckboxCardsComponentChecked = Template.bind({}); 46 | CheckboxCardsComponentChecked.args = { 47 | title: 'CheckboxCards', 48 | value: ['default1', 'default2'], 49 | }; 50 | 51 | export const CheckboxCardsComponentRow = Template.bind({}); 52 | CheckboxCardsComponentRow.args = { 53 | title: 'CheckboxCards', 54 | direction: 'row', 55 | }; 56 | 57 | export const CheckboxCardsComponentDisabled = Template.bind({}); 58 | CheckboxCardsComponentDisabled.args = { 59 | title: 'CheckboxCards', 60 | disabled: true, 61 | }; 62 | 63 | export const CheckboxCardsComponentWithInput = Template.bind({}); 64 | CheckboxCardsComponentWithInput.args = { 65 | title: 'CheckboxCards', 66 | hideInput: false, 67 | }; 68 | 69 | export const CheckboxCardsComponentError = Template.bind({}); 70 | CheckboxCardsComponentError.args = { 71 | title: 'CheckboxCards with error', 72 | error: 'Error Message', 73 | }; 74 | 75 | const Component = { 76 | title: 'Form/CheckboxCards', 77 | component: CheckboxCardsComponent, 78 | }; 79 | 80 | export default Component; 81 | -------------------------------------------------------------------------------- /src/CheckboxCards/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useCallback, useMemo } from 'react'; 2 | import cx from 'clsx'; 3 | import Checkbox from '../Checkbox'; 4 | import s from './CheckboxCards.module.scss'; 5 | import { FieldError } from 'react-hook-form'; 6 | 7 | interface Option { 8 | value: string | number; 9 | label: string | React.ReactNode; 10 | } 11 | 12 | interface CheckboxCardsProps { 13 | name: string; 14 | onChange: (event: { 15 | target: { name: string; value: (string | number)[] }; 16 | }) => void; 17 | value?: (string | number)[]; 18 | options: Option[]; 19 | label?: string | React.ReactNode | null; 20 | inputClassName?: string | null; 21 | cardClassName?: string | null; 22 | labelClassName?: string | null; 23 | optionLabelClassName?: string | null; 24 | errorClassName?: string | null; 25 | direction?: 'row' | 'column'; 26 | hideInput?: boolean; 27 | disabled?: boolean; 28 | error?: 29 | | boolean 30 | | string 31 | | { message?: string } 32 | | null 33 | | undefined 34 | | FieldError; 35 | } 36 | 37 | const CheckboxCards = forwardRef( 38 | ( 39 | { 40 | name, 41 | onChange, 42 | value = [], 43 | options = [], 44 | label = null, 45 | inputClassName = null, 46 | cardClassName = null, 47 | labelClassName = null, 48 | optionLabelClassName = null, 49 | direction = 'column', 50 | hideInput = true, 51 | errorClassName = null, 52 | disabled = false, 53 | error = null, 54 | }, 55 | inputRef 56 | ) => { 57 | const handleChange = useCallback( 58 | (checked: boolean, optionValue: string | number) => { 59 | let newValue: (string | number)[]; 60 | if (checked) { 61 | newValue = [...value, optionValue]; 62 | } else { 63 | newValue = value?.filter((val) => val !== optionValue); 64 | } 65 | 66 | onChange({ 67 | target: { 68 | name, 69 | value: newValue, 70 | }, 71 | }); 72 | }, 73 | [onChange, value, name] 74 | ); 75 | 76 | const errorMessage = useMemo(() => { 77 | if (typeof error === 'string') { 78 | return error; 79 | } else if (error && typeof error === 'object' && error.message) { 80 | return error.message; 81 | } 82 | return null; 83 | }, [error]); 84 | 85 | return ( 86 | 145 | ); 146 | } 147 | ); 148 | 149 | CheckboxCards.displayName = 'CheckboxCards'; 150 | 151 | export default CheckboxCards; 152 | 153 | export { type CheckboxCardsProps }; 154 | -------------------------------------------------------------------------------- /src/CheckboxGroup/CheckboxGroup.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | pointer-events: all; 3 | position: relative; 4 | display: flex; 5 | flex-direction: column; 6 | margin-bottom: var(--rfe-spacing); 7 | 8 | & label { 9 | margin-bottom: var(--rfe-label-spacing); 10 | } 11 | 12 | & label:last-of-type { 13 | margin-bottom: 0; 14 | } 15 | } 16 | 17 | .label { 18 | font-size: var(--rfe-font-size); 19 | line-height: 1; 20 | color: var(--rfe-color); 21 | margin-bottom: var(--rfe-label-spacing); 22 | } 23 | 24 | .errorLabel { 25 | color: var(--rfe-color-error); 26 | font-size: var(--rfe-error-font-size); 27 | margin-top: var(--rfe-label-spacing); 28 | } 29 | -------------------------------------------------------------------------------- /src/CheckboxGroup/CheckboxGroup.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Index from './index'; 4 | 5 | const Template = (args) => { 6 | const [inputs, setInputs] = useState({}); 7 | const onChange = (e) => { 8 | const { name, value } = e.target; 9 | 10 | setInputs((prevState) => ({ 11 | ...prevState, 12 | [name]: value, 13 | })); 14 | }; 15 | 16 | return ( 17 | <> 18 |

{args.title}

19 |
26 | 37 |
38 | 39 | ); 40 | }; 41 | 42 | export const CheckboxGroupComponent = Template.bind({}); 43 | CheckboxGroupComponent.args = { title: 'CheckboxGroup' }; 44 | 45 | export const CheckboxGroupComponentChecked = Template.bind({}); 46 | CheckboxGroupComponentChecked.args = { 47 | title: 'CheckboxGroup', 48 | value: ['default1', 'default2'], 49 | }; 50 | 51 | export const CheckboxGroupComponentDisabled = Template.bind({}); 52 | CheckboxGroupComponentDisabled.args = { 53 | title: 'CheckboxGroup', 54 | disabled: true, 55 | }; 56 | 57 | export const CheckboxGroupComponentError = Template.bind({}); 58 | CheckboxGroupComponentError.args = { 59 | title: 'CheckboxGroup with error', 60 | error: 'Error Message', 61 | }; 62 | 63 | const Component = { 64 | title: 'Form/CheckboxGroup', 65 | component: CheckboxGroupComponent, 66 | }; 67 | 68 | export default Component; 69 | -------------------------------------------------------------------------------- /src/CheckboxGroup/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useCallback, useMemo } from 'react'; 2 | import cx from 'clsx'; 3 | import Checkbox from '../Checkbox'; 4 | import s from './CheckboxGroup.module.scss'; 5 | import { FieldError } from 'react-hook-form'; 6 | 7 | interface Option { 8 | value: string | number; 9 | label: string | React.ReactNode; 10 | } 11 | 12 | interface CheckboxGroupProps { 13 | name: string; 14 | onChange: (event: { 15 | target: { name: string; value: (string | number)[] }; 16 | }) => void; 17 | value?: (string | number)[]; 18 | options: Option[]; 19 | label?: string | React.ReactNode | null; 20 | inputClassName?: string | null; 21 | labelClassName?: string | null; 22 | optionLabelClassName?: string | null; 23 | errorClassName?: string | null; 24 | disabled?: boolean; 25 | error?: 26 | | boolean 27 | | string 28 | | { message?: string } 29 | | null 30 | | undefined 31 | | FieldError; 32 | } 33 | 34 | const CheckboxGroup = forwardRef( 35 | ( 36 | { 37 | name, 38 | onChange, 39 | value = [], 40 | options = [], 41 | label = null, 42 | inputClassName = null, 43 | labelClassName = null, 44 | optionLabelClassName = null, 45 | errorClassName = null, 46 | disabled = false, 47 | error = null, 48 | }, 49 | inputRef 50 | ) => { 51 | const handleChange = useCallback( 52 | (checked: boolean, optionValue: string | number) => { 53 | let newValue: (string | number)[]; 54 | if (checked) { 55 | newValue = [...value, optionValue]; 56 | } else { 57 | newValue = value.filter((val) => val !== optionValue); 58 | } 59 | 60 | onChange({ 61 | target: { 62 | name, 63 | value: newValue, 64 | }, 65 | }); 66 | }, 67 | [onChange, value, name] 68 | ); 69 | 70 | const errorMessage = useMemo(() => { 71 | if (typeof error === 'string') { 72 | return error; 73 | } else if (error && typeof error === 'object' && error.message) { 74 | return error.message; 75 | } 76 | return null; 77 | }, [error]); 78 | 79 | return ( 80 | 117 | ); 118 | } 119 | ); 120 | 121 | CheckboxGroup.displayName = 'CheckboxGroup'; 122 | 123 | export default CheckboxGroup; 124 | 125 | export { type CheckboxGroupProps }; 126 | -------------------------------------------------------------------------------- /src/DatePicker/DatePicker.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-bottom: var(--rfe-spacing); 3 | } 4 | 5 | .inputRoot { 6 | position: relative; 7 | } 8 | 9 | .calendar { 10 | margin: var(--rfe-label-spacing) 0 !important; 11 | } 12 | 13 | .label { 14 | font-size: var(--rfe-font-size); 15 | line-height: 1; 16 | color: var(--rfe-color); 17 | margin-bottom: 0; 18 | position: absolute; 19 | top: -2px; 20 | left: 16px; 21 | cursor: text; 22 | transform: translateY(calc((var(--rfe-input-height) - 14px) / 2)); 23 | transition: all 250ms; 24 | } 25 | 26 | .label.disableShrink { 27 | display: block; 28 | position: static; 29 | margin-bottom: var(--rfe-label-spacing); 30 | color: var(--rfe-color); 31 | transform: none; 32 | } 33 | 34 | .prepend ~ .placeholder { 35 | padding: 24px 16px 0 60px !important; 36 | } 37 | 38 | .disabledLabel { 39 | pointer-events: none; 40 | cursor: not-allowed; 41 | } 42 | 43 | .placeholder { 44 | padding: 19px 16px 0 !important; 45 | } 46 | 47 | .labelPlaceholder { 48 | font-size: var(--rfe-focused-font-size); 49 | transform: translateY(9px); 50 | top: 0; 51 | } 52 | 53 | .focused { 54 | box-shadow: var(--rfe-focus); 55 | } 56 | 57 | .input { 58 | width: 100%; 59 | line-height: 24px; 60 | color: var(--rfe-color); 61 | padding: 19px 16px 0; 62 | background: var(--rfe-background); 63 | border: 1px solid var(--rfe-border); 64 | border-radius: var(--rfe-border-radius); 65 | height: var(--rfe-input-height); 66 | transition: all 250ms; 67 | font-family: var(--rfe-font-family); 68 | } 69 | 70 | .input:focus { 71 | box-shadow: var(--rfe-focus); 72 | } 73 | 74 | .input::placeholder { 75 | color: var(--rfe-color-placeholder); 76 | font-size: var(--rfe-placeholder-font-size); 77 | } 78 | 79 | .input:-webkit-autofill:not(.disableShrink) ~ .label, 80 | .input:focus:not(.disableShrink) ~ .label, 81 | .input:not([value=''], .disableShrink) ~ .label { 82 | font-size: var(--rfe-focused-font-size); 83 | transform: translateY(9px); 84 | } 85 | 86 | .input.disableShrink { 87 | padding: 16px; 88 | } 89 | 90 | .append, 91 | .prepend { 92 | position: absolute; 93 | top: 50%; 94 | color: var(--rfe-color); 95 | transform: translateY(-50%); 96 | display: flex; 97 | align-items: center; 98 | justify-content: center; 99 | pointer-events: none; 100 | } 101 | 102 | .prepend { 103 | left: 16px; 104 | font-size: var(--rfe-placeholder-font-size); 105 | line-height: 16px; 106 | padding-right: 13px; 107 | border-right: 1px solid var(--rfe-border); 108 | height: var(--rfe-input-height); 109 | } 110 | 111 | .prepend ~ .label { 112 | left: 62px; 113 | } 114 | 115 | .prepend ~ div > .input { 116 | padding: 20px 16px 0 60px; 117 | } 118 | 119 | .prepend ~ div > .disableShrink { 120 | padding: 16px 16px 16px 60px; 121 | } 122 | 123 | .append { 124 | right: 0; 125 | height: 56px; 126 | min-width: 36px; 127 | } 128 | 129 | .append ~ .input { 130 | padding: 19px 56px 0 16px; 131 | } 132 | 133 | .append ~ .input.disableShrink { 134 | padding: 0 50px 0 16px; 135 | } 136 | 137 | .appendDisabledShrink { 138 | top: 26px; 139 | transform: none; 140 | } 141 | 142 | .prependDisabledShrink { 143 | top: 26px; 144 | transform: none; 145 | } 146 | 147 | .errorLabel { 148 | color: var(--rfe-color-error); 149 | font-size: var(--rfe-error-font-size); 150 | margin-top: var(--rfe-label-spacing); 151 | pointer-events: none; 152 | } 153 | 154 | .inputError { 155 | border-color: var(--rfe-color-error) !important; 156 | } 157 | 158 | .disabled { 159 | background: var(--rfe-background-disabled); 160 | color: var(--rfe-color); 161 | cursor: not-allowed; 162 | opacity: 1; 163 | } 164 | 165 | .icon { 166 | background: none; 167 | border: 0; 168 | margin: 0 8px; 169 | height: 100%; 170 | display: flex; 171 | align-items: center; 172 | justify-content: center; 173 | pointer-events: all; 174 | cursor: pointer; 175 | } 176 | 177 | .popper { 178 | z-index: 1000; 179 | } 180 | -------------------------------------------------------------------------------- /src/DatePicker/DatePicker.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import DatePicker from './index'; 4 | 5 | const Template = (arg) => { 6 | const [inputs, setInputs] = useState({}); 7 | const onChange = (e) => { 8 | const { name, value, type } = e.target; 9 | 10 | setInputs((prevState) => ({ 11 | ...prevState, 12 | [name]: type === 'checkbox' ? !prevState[name] : value, 13 | })); 14 | }; 15 | 16 | return ( 17 | <> 18 |

{arg.title}

19 | 26 | 27 | ); 28 | }; 29 | export const DatePickerComponent = Template.bind({}); 30 | DatePickerComponent.args = { title: 'DatePicker' }; 31 | 32 | const Component = { 33 | title: 'Form/DatePicker', 34 | component: DatePicker, 35 | }; 36 | 37 | export default Component; 38 | -------------------------------------------------------------------------------- /src/DateRangePicker/DateRangePicker.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-bottom: var(--rfe-spacing); 3 | } 4 | 5 | .inputRoot { 6 | position: relative; 7 | } 8 | 9 | .calendar { 10 | margin: var(--rfe-label-spacing) 0 !important; 11 | } 12 | 13 | .label { 14 | font-size: var(--rfe-font-size); 15 | line-height: 1; 16 | color: var(--rfe-color); 17 | margin-bottom: 0; 18 | position: absolute; 19 | top: -2px; 20 | left: 16px; 21 | cursor: text; 22 | transform: translateY(calc((var(--rfe-input-height) - 14px) / 2)); 23 | transition: all 250ms; 24 | } 25 | 26 | .label.disableShrink { 27 | display: block; 28 | position: static; 29 | margin-bottom: var(--rfe-label-spacing); 30 | color: var(--rfe-color); 31 | transform: none; 32 | } 33 | 34 | .prepend ~ .placeholder { 35 | padding: 24px 16px 0 60px !important; 36 | } 37 | 38 | .placeholder { 39 | padding: 19px 16px 0 !important; 40 | } 41 | 42 | .disabledLabel { 43 | pointer-events: none; 44 | cursor: not-allowed; 45 | } 46 | 47 | .labelPlaceholder { 48 | font-size: var(--rfe-focused-font-size); 49 | transform: translateY(9px); 50 | top: 0; 51 | } 52 | 53 | .input { 54 | width: 100%; 55 | line-height: 24px; 56 | color: var(--rfe-color); 57 | padding: 19px 30px 0 16px; 58 | background: var(--rfe-background); 59 | border: 1px solid var(--rfe-border); 60 | border-radius: var(--rfe-border-radius); 61 | height: var(--rfe-input-height); 62 | transition: all 250ms; 63 | font-family: var(--rfe-font-family); 64 | } 65 | 66 | .input:focus { 67 | box-shadow: var(--rfe-focus); 68 | } 69 | 70 | .input::placeholder { 71 | color: var(--rfe-color-placeholder); 72 | font-size: var(--rfe-placeholder-font-size); 73 | } 74 | 75 | .input:-webkit-autofill:not(.disableShrink) ~ .label, 76 | .input:focus:not(.disableShrink) ~ .label, 77 | .input:not([value=''], .disableShrink) ~ .label { 78 | font-size: var(--rfe-focused-font-size); 79 | transform: translateY(9px); 80 | } 81 | 82 | .input.disableShrink { 83 | padding: 16px; 84 | } 85 | 86 | .append, 87 | .prepend { 88 | position: absolute; 89 | top: 50%; 90 | color: var(--rfe-color); 91 | transform: translateY(-50%); 92 | display: flex; 93 | align-items: center; 94 | justify-content: center; 95 | pointer-events: none; 96 | } 97 | 98 | .prepend { 99 | left: 16px; 100 | font-size: var(--rfe-placeholder-font-size); 101 | line-height: 16px; 102 | padding-right: 13px; 103 | border-right: 1px solid var(--rfe-border); 104 | height: var(--rfe-input-height); 105 | } 106 | 107 | .prepend ~ .label { 108 | left: 62px; 109 | } 110 | 111 | .prepend ~ div > .input { 112 | padding: 20px 16px 0 60px; 113 | } 114 | 115 | .prepend ~ div > .disableShrink { 116 | padding: 16px 16px 16px 60px; 117 | } 118 | 119 | .append { 120 | right: 0; 121 | height: 56px; 122 | min-width: 36px; 123 | } 124 | 125 | .append ~ .input { 126 | padding: 19px 56px 0 16px; 127 | } 128 | 129 | .append ~ .input.disableShrink { 130 | padding: 0 50px 0 16px; 131 | } 132 | 133 | .appendDisabledShrink { 134 | top: 26px; 135 | transform: none; 136 | } 137 | 138 | .prependDisabledShrink { 139 | top: 26px; 140 | transform: none; 141 | } 142 | 143 | .errorLabel { 144 | color: var(--rfe-color-error); 145 | font-size: var(--rfe-error-font-size); 146 | margin-top: var(--rfe-label-spacing); 147 | pointer-events: none; 148 | } 149 | 150 | .inputError { 151 | border-color: var(--rfe-color-error) !important; 152 | } 153 | 154 | .disabled { 155 | background: var(--rfe-background-disabled); 156 | color: var(--rfe-color); 157 | cursor: not-allowed; 158 | opacity: 1; 159 | } 160 | 161 | .focused { 162 | box-shadow: var(--rfe-focus); 163 | } 164 | 165 | .icon { 166 | background: none; 167 | border: 0; 168 | margin: 0 8px; 169 | height: 100%; 170 | display: flex; 171 | align-items: center; 172 | justify-content: center; 173 | pointer-events: all; 174 | cursor: pointer; 175 | } 176 | 177 | .popper { 178 | z-index: 1000; 179 | } 180 | -------------------------------------------------------------------------------- /src/DateRangePicker/DateRangePicker.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import DateRangePicker from './index'; 4 | 5 | const Template = (arg) => { 6 | const [inputs, setInputs] = useState({}); 7 | const onChange = (e) => { 8 | const { name, value, type } = e.target; 9 | 10 | setInputs((prevState) => ({ 11 | ...prevState, 12 | [name]: type === 'checkbox' ? !prevState[name] : value, 13 | })); 14 | }; 15 | 16 | return ( 17 | <> 18 |

{arg.title}

19 | 26 | 27 | ); 28 | }; 29 | export const DatePickerComponent = Template.bind({}); 30 | DatePickerComponent.args = { title: 'DateRangePicker' }; 31 | 32 | const Component = { 33 | title: 'Form/DateRangePicker', 34 | component: DateRangePicker, 35 | }; 36 | 37 | export default Component; 38 | -------------------------------------------------------------------------------- /src/Form/Form.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-bottom: 16px; 3 | } 4 | -------------------------------------------------------------------------------- /src/Form/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, HTMLProps } from 'react'; 2 | import cx from 'clsx'; 3 | import s from './Form.module.scss'; 4 | 5 | interface FormProps extends HTMLProps { 6 | children: ReactNode; 7 | } 8 | 9 | const Form: React.FC = ({ children, ...rest }) => { 10 | return ( 11 |
12 | {children} 13 |
14 | ); 15 | }; 16 | 17 | export default Form; 18 | 19 | export { type FormProps }; 20 | -------------------------------------------------------------------------------- /src/Icon/CheckIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const SvgComponent: React.FC> = (props) => ( 4 | 11 | 18 | 19 | ); 20 | export default SvgComponent; 21 | -------------------------------------------------------------------------------- /src/Icon/PieIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const SvgComponent: React.FC> = (props) => ( 4 | 11 | 15 | 16 | ); 17 | 18 | export default SvgComponent; 19 | -------------------------------------------------------------------------------- /src/Icon/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PieIcon } from './PieIcon'; 2 | export { default as CheckIcon } from './CheckIcon'; 3 | -------------------------------------------------------------------------------- /src/Input/Input.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-bottom: var(--rfe-spacing); 3 | } 4 | 5 | .inputRoot { 6 | position: relative; 7 | } 8 | 9 | .label { 10 | font-size: var(--rfe-font-size); 11 | line-height: 1; 12 | color: var(--rfe-color); 13 | margin-bottom: 0; 14 | position: absolute; 15 | top: -2px; 16 | left: 16px; 17 | cursor: text; 18 | transform: translateY(calc((var(--rfe-input-height) - 14px) / 2)); 19 | transition: all 250ms; 20 | } 21 | 22 | .label.disableShrink { 23 | display: block; 24 | position: static; 25 | margin-bottom: var(--rfe-label-spacing); 26 | color: var(--rfe-color); 27 | transform: none; 28 | } 29 | 30 | .prepend ~ .placeholder { 31 | padding: 24px 16px 0 60px !important; 32 | } 33 | 34 | .placeholder { 35 | padding: 19px 16px 0 !important; 36 | } 37 | 38 | .labelPlaceholder { 39 | font-size: var(--rfe-focused-font-size); 40 | transform: translateY(9px); 41 | top: 0; 42 | } 43 | 44 | .input { 45 | width: 100%; 46 | line-height: 24px; 47 | color: var(--rfe-color); 48 | padding: 19px 16px 0; 49 | background: var(--rfe-background); 50 | border: 1px solid var(--rfe-border); 51 | border-radius: var(--rfe-border-radius); 52 | height: var(--rfe-input-height); 53 | transition: all 250ms; 54 | font-family: var(--rfe-font-family); 55 | } 56 | 57 | .input:focus { 58 | box-shadow: var(--rfe-focus); 59 | } 60 | 61 | .input::placeholder { 62 | color: var(--rfe-color-placeholder); 63 | font-size: var(--rfe-placeholder-font-size); 64 | } 65 | 66 | .input:-webkit-autofill:not(.disableShrink) ~ .label, 67 | .input:focus:not(.disableShrink) ~ .label, 68 | .input:not([value=''], .disableShrink) ~ .label { 69 | font-size: var(--rfe-focused-font-size); 70 | transform: translateY(9px); 71 | } 72 | 73 | .input.disableShrink { 74 | padding: 16px; 75 | } 76 | 77 | .disabledLabel { 78 | pointer-events: none; 79 | cursor: not-allowed; 80 | } 81 | 82 | .append, 83 | .prepend { 84 | position: absolute; 85 | top: 50%; 86 | color: var(--rfe-color); 87 | transform: translateY(-50%); 88 | display: flex; 89 | align-items: center; 90 | justify-content: center; 91 | pointer-events: none; 92 | } 93 | 94 | .prepend { 95 | left: 16px; 96 | font-size: var(--rfe-placeholder-font-size); 97 | line-height: 16px; 98 | padding-right: 13px; 99 | border-right: 1px solid var(--rfe-border); 100 | height: var(--rfe-input-height); 101 | } 102 | 103 | .prepend ~ .label { 104 | left: 62px; 105 | } 106 | 107 | .prepend ~ .input { 108 | padding: 20px 16px 0 60px !important; 109 | } 110 | 111 | .prepend ~ .input.disableShrink { 112 | padding: 16px 16px 16px 60px !important; 113 | } 114 | 115 | .append { 116 | right: 0; 117 | height: 56px; 118 | min-width: 36px; 119 | } 120 | 121 | .append ~ .input { 122 | padding: 19px 56px 0 16px; 123 | } 124 | 125 | .append ~ .input.disableShrink { 126 | padding: 0 50px 0 16px; 127 | } 128 | 129 | .appendDisabledShrink { 130 | top: 26px; 131 | transform: none; 132 | } 133 | 134 | .prependDisabledShrink { 135 | top: 26px; 136 | transform: none; 137 | } 138 | 139 | .errorLabel { 140 | color: var(--rfe-color-error); 141 | font-size: var(--rfe-error-font-size); 142 | margin-top: var(--rfe-label-spacing); 143 | pointer-events: none; 144 | } 145 | 146 | .inputError { 147 | border-color: var(--rfe-color-error) !important; 148 | } 149 | 150 | .disabled { 151 | background: var(--rfe-background-disabled); 152 | color: var(--rfe-color); 153 | cursor: not-allowed; 154 | opacity: 1; 155 | } 156 | -------------------------------------------------------------------------------- /src/Input/Input.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { PieIcon } from '../Icon'; 4 | import Index from './index'; 5 | 6 | const Template = (arg) => { 7 | const [inputs, setInputs] = useState({}); 8 | const onChange = (e) => { 9 | const { name, value, type } = e.target; 10 | 11 | setInputs((prevState) => ({ 12 | ...prevState, 13 | [name]: type === 'checkbox' ? !prevState[name] : value, 14 | })); 15 | }; 16 | 17 | return ( 18 | <> 19 |

{arg.title}

20 | 21 | 22 | ); 23 | }; 24 | export const InputComponent = Template.bind({}); 25 | InputComponent.args = { title: 'Input' }; 26 | 27 | export const InputWithLabel = Template.bind({}); 28 | InputWithLabel.args = { title: 'Input with label', label: 'Label' }; 29 | 30 | export const InputWithPlaceholder = Template.bind({}); 31 | InputWithPlaceholder.args = { 32 | title: 'Input with placeholder', 33 | placeholder: 'Placeholder', 34 | }; 35 | 36 | export const InputWithPlaceholderAndLabel = Template.bind({}); 37 | InputWithPlaceholderAndLabel.args = { 38 | title: 'Input with placeholder and label', 39 | placeholder: 'Placeholder', 40 | label: 'Label', 41 | }; 42 | 43 | export const InputWithPlaceholderAndLabelAndDisableShrink = Template.bind({}); 44 | InputWithPlaceholderAndLabelAndDisableShrink.args = { 45 | title: 'Input with placeholder and label and disable shrink', 46 | placeholder: 'Placeholder', 47 | label: 'Label', 48 | disableShrink: true, 49 | }; 50 | 51 | export const InputDisabled = Template.bind({}); 52 | InputDisabled.args = { 53 | title: 'Disabled Input', 54 | placeholder: 'Placeholder', 55 | disabled: true, 56 | label: 'Label', 57 | }; 58 | 59 | export const InputAppend = Template.bind({}); 60 | InputAppend.args = { 61 | title: 'Input with append', 62 | placeholder: 'Placeholder', 63 | label: 'Label', 64 | append: , 65 | }; 66 | 67 | export const InputPrepend = Template.bind({}); 68 | InputPrepend.args = { 69 | title: 'Input with prepend', 70 | placeholder: 'Placeholder', 71 | label: 'Label', 72 | prepend: , 73 | }; 74 | 75 | export const InputError = Template.bind({}); 76 | InputError.args = { 77 | title: 'Input with error', 78 | label: 'Label', 79 | error: 'Error Message', 80 | }; 81 | 82 | const Component = { 83 | title: 'Form/Input', 84 | component: Index, 85 | }; 86 | 87 | export default Component; 88 | -------------------------------------------------------------------------------- /src/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | forwardRef, 3 | useCallback, 4 | useMemo, 5 | ChangeEvent, 6 | RefObject, 7 | } from 'react'; 8 | import cx from 'clsx'; 9 | import s from './Input.module.scss'; 10 | import { FieldError } from 'react-hook-form'; 11 | 12 | export interface DateRangePickerInputProps { 13 | from: Date; 14 | to: Date; 15 | } 16 | 17 | type InputProps = { 18 | name: string; 19 | onChange: (e: ChangeEvent) => void; 20 | error?: 21 | | boolean 22 | | string 23 | | { message?: string } 24 | | null 25 | | undefined 26 | | FieldError; 27 | label?: string | null; 28 | placeholder?: string | undefined; 29 | value?: string | boolean | number | Date | Date[] | DateRangePickerInputProps; 30 | inputClassName?: string | null; 31 | labelClassName?: string | null; 32 | errorClassName?: string | null; 33 | prepend?: React.ReactNode | JSX.Element | null; 34 | prependClassName?: string | null; 35 | append?: React.ReactNode | JSX.Element | null; 36 | appendClassName?: string | null; 37 | disableShrink?: boolean; 38 | disabled?: boolean; 39 | } & React.InputHTMLAttributes; 40 | 41 | const Input = forwardRef( 42 | ( 43 | { 44 | name, 45 | onChange, 46 | error = null, 47 | label = null, 48 | placeholder = undefined, 49 | value = '', 50 | inputClassName = null, 51 | labelClassName = null, 52 | errorClassName = null, 53 | prepend = null, 54 | prependClassName = null, 55 | append = null, 56 | appendClassName = null, 57 | disableShrink = false, 58 | disabled = false, 59 | ...rest 60 | }, 61 | inputRef: RefObject 62 | ) => { 63 | const handleChange = useCallback( 64 | (e: ChangeEvent) => { 65 | onChange(e); 66 | }, 67 | [onChange] 68 | ); 69 | 70 | const errorMessage = useMemo(() => { 71 | let message: string | null = null; 72 | if (error && typeof error === 'string') { 73 | message = error; 74 | } else if (error && typeof error === 'object' && error?.message) { 75 | message = error.message; 76 | } 77 | return message; 78 | }, [error]); 79 | 80 | const input = useMemo(() => { 81 | return ( 82 | 99 | ); 100 | }, [ 101 | disableShrink, 102 | label, 103 | placeholder, 104 | disabled, 105 | error, 106 | inputClassName, 107 | name, 108 | value, 109 | inputRef, 110 | handleChange, 111 | rest, 112 | ]); 113 | 114 | const labelEl = useMemo( 115 | () => ( 116 | 150 | ), 151 | [disabled, name, disableShrink, label, placeholder, labelClassName] 152 | ); 153 | 154 | return ( 155 |
156 |
157 | {prepend && ( 158 |
164 | {prepend} 165 |
166 | )} 167 | 168 | {append && ( 169 |
175 | {append} 176 |
177 | )} 178 | 179 | {label && disableShrink ? labelEl : null} 180 | 181 | {input} 182 | 183 | {label && !disableShrink ? labelEl : null} 184 |
185 | {errorMessage ? ( 186 | 191 | {errorMessage} 192 | 193 | ) : null} 194 |
195 | ); 196 | } 197 | ); 198 | 199 | Input.displayName = 'Input'; 200 | 201 | export default Input; 202 | 203 | export { type InputProps }; 204 | -------------------------------------------------------------------------------- /src/MultipleDatePicker/MultipleDatePicker.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-bottom: var(--rfe-spacing); 3 | position: relative; 4 | } 5 | 6 | .inputRoot { 7 | position: relative; 8 | } 9 | 10 | .calendar { 11 | margin: var(--rfe-label-spacing) 0 !important; 12 | } 13 | 14 | .label { 15 | line-height: 1; 16 | font-size: var(--rfe-font-size); 17 | color: var(--rfe-color); 18 | margin-bottom: 0; 19 | position: absolute; 20 | left: 16px; 21 | top: -2px; 22 | cursor: text; 23 | transform: translateY(calc((var(--rfe-input-height) - 14px) / 2)); 24 | transition: all 250ms; 25 | } 26 | 27 | .label.disableShrink { 28 | display: block; 29 | position: static; 30 | margin-bottom: var(--rfe-label-spacing); 31 | color: var(--rfe-color); 32 | transform: none; 33 | } 34 | 35 | .disabledLabel { 36 | pointer-events: none; 37 | cursor: not-allowed; 38 | } 39 | 40 | .labelPlaceholder { 41 | font-size: var(--rfe-focused-font-size); 42 | transform: translateY(9px); 43 | top: 0; 44 | } 45 | 46 | .labelFocused { 47 | font-size: var(--rfe-focused-font-size); 48 | transform: translateY(9px); 49 | top: 0; 50 | } 51 | 52 | .input { 53 | width: 100%; 54 | line-height: 24px; 55 | background-color: var(--rfe-background) !important; 56 | border: 1px solid var(--rfe-border) !important; 57 | border-radius: var(--rfe-border-radius); 58 | min-height: var(--rfe-input-height) !important; 59 | transition: all 250ms !important; 60 | font-family: var(--rfe-font-family); 61 | 62 | &:hover { 63 | border: 1px solid var(--rfe-border); 64 | } 65 | } 66 | 67 | .focus { 68 | box-shadow: var(--rfe-focus) !important; 69 | } 70 | 71 | .notFocus { 72 | box-shadow: none !important; 73 | } 74 | 75 | .inputError { 76 | border-color: var(--rfe-color-error) !important; 77 | } 78 | 79 | .input:-webkit-autofill:not(.disableShrink) ~ .label, 80 | .input:focus:not(.disableShrink) ~ .label, 81 | .input:not([value=''], .disableShrink) ~ .label { 82 | font-size: var(--rfe-focused-font-size); 83 | transform: translateY(9px); 84 | } 85 | 86 | .input.disableShrink { 87 | padding: 16px; 88 | } 89 | 90 | .errorLabel { 91 | color: var(--rfe-color-error); 92 | font-size: var(--rfe-error-font-size); 93 | margin-top: var(--rfe-label-spacing); 94 | pointer-events: none; 95 | } 96 | 97 | .disabled { 98 | background: var(--rfe-background-disabled) !important; 99 | color: var(--rfe-color) !important; 100 | cursor: not-allowed !important; 101 | pointer-events: all; 102 | } 103 | 104 | .innerInput { 105 | margin-left: 8px !important; 106 | color: var(--rfe-color) !important; 107 | } 108 | 109 | .control { 110 | align-items: flex-end !important; 111 | } 112 | 113 | .menu { 114 | background-color: var(--rfe-background) !important; 115 | border: 1px solid var(--rfe-border); 116 | border-radius: var(--rfe-border-radius); 117 | padding: 0 !important; 118 | margin: 6px 0 0 !important; 119 | z-index: 9999; 120 | } 121 | 122 | .option { 123 | color: var(--rfe-color) !important; 124 | 125 | &:hover, 126 | &:focus, 127 | &:active, 128 | &:focus-visible, 129 | &:focus-within { 130 | background-color: var(--rfe-background-selected) !important; 131 | } 132 | } 133 | 134 | .optionFocused { 135 | color: var(--rfe-color) !important; 136 | background-color: var(--rfe-background-selected) !important; 137 | } 138 | 139 | .singleValue { 140 | color: var(--rfe-color) !important; 141 | margin-left: 8px !important; 142 | } 143 | 144 | .placeholder { 145 | color: var(--rfe-color-placeholder) !important; 146 | font-size: var(--rfe-placeholder-font-size); 147 | margin-left: 8px !important; 148 | } 149 | 150 | .indicatorsContainer { 151 | min-width: 36px; 152 | display: flex; 153 | justify-content: center; 154 | } 155 | 156 | .indicatorSeparator { 157 | display: none; 158 | } 159 | 160 | .indicatorsContainer, 161 | .clearIndicator, 162 | .multiValueRemove { 163 | & svg { 164 | color: var(--rfe-color); 165 | pointer-events: all; 166 | cursor: pointer; 167 | } 168 | } 169 | 170 | .multiValue { 171 | height: 24px; 172 | border-radius: var(--rfe-border-radius) !important; 173 | background-color: var(--rfe-background-selected) !important; 174 | align-items: center; 175 | 176 | & div { 177 | color: var(--rfe-color) !important; 178 | } 179 | } 180 | 181 | .multiValueRemove { 182 | color: var(--rfe-color) !important; 183 | 184 | &:hover { 185 | background: unset !important; 186 | } 187 | } 188 | 189 | .valueContainer { 190 | padding-top: 20px !important; 191 | } 192 | 193 | .popper { 194 | z-index: 1000; 195 | } 196 | 197 | .icon { 198 | background: none; 199 | border: 0; 200 | margin: 0; 201 | height: 100%; 202 | display: flex; 203 | align-items: center; 204 | justify-content: center; 205 | pointer-events: all; 206 | cursor: pointer; 207 | } 208 | -------------------------------------------------------------------------------- /src/MultipleDatePicker/MultipleDatePicker.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Index from './index'; 4 | 5 | const Template = (arg) => { 6 | const [inputs, setInputs] = useState({}); 7 | const onChange = (e) => { 8 | const { name, value, type } = e.target; 9 | 10 | setInputs((prevState) => ({ 11 | ...prevState, 12 | [name]: type === 'checkbox' ? !prevState[name] : value, 13 | })); 14 | }; 15 | 16 | return ( 17 | <> 18 |

{arg.title}

19 | 20 | 21 | ); 22 | }; 23 | export const DatePickerComponent = Template.bind({}); 24 | DatePickerComponent.args = { title: 'MultipleDatePicker' }; 25 | 26 | const Component = { 27 | title: 'Form/MultipleDatePicker', 28 | component: Index, 29 | }; 30 | 31 | export default Component; 32 | -------------------------------------------------------------------------------- /src/NumberInput/NumberInput.module.scss: -------------------------------------------------------------------------------- 1 | .buttons { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .button { 7 | color: var(--rfe-color); 8 | padding: 0 4px; 9 | margin: 0 4px; 10 | background: none; 11 | border: 0; 12 | border-radius: var(--rfe-border-radius); 13 | height: 100%; 14 | display: flex; 15 | align-items: center; 16 | justify-content: center; 17 | pointer-events: all; 18 | cursor: pointer; 19 | } 20 | -------------------------------------------------------------------------------- /src/NumberInput/NumberInput.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Index from './index'; 4 | 5 | const Template = (arg) => { 6 | const [inputs, setInputs] = useState({}); 7 | const onChange = (e) => { 8 | const { name, value, type } = e.target; 9 | 10 | setInputs((prevState) => ({ 11 | ...prevState, 12 | [name]: type === 'checkbox' ? !prevState[name] : value, 13 | })); 14 | }; 15 | 16 | return ( 17 | <> 18 |

{arg.title}

19 | 20 | 21 | ); 22 | }; 23 | 24 | export const NumberInputComponent = Template.bind({}); 25 | NumberInputComponent.args = { title: 'NumberInput' }; 26 | 27 | export const NumberInputWithLabel = Template.bind({}); 28 | NumberInputWithLabel.args = { 29 | title: 'NumberInput with label', 30 | label: 'Label', 31 | }; 32 | 33 | export const NumberInputWithPlaceholder = Template.bind({}); 34 | NumberInputWithPlaceholder.args = { 35 | title: 'NumberInput with placeholder', 36 | placeholder: 'Placeholder', 37 | }; 38 | 39 | export const NumberInputWithPlaceholderAndLabel = Template.bind({}); 40 | NumberInputWithPlaceholderAndLabel.args = { 41 | title: 'NumberInput with placeholder and label', 42 | placeholder: 'Placeholder', 43 | label: 'Label', 44 | }; 45 | 46 | export const NumberInputWithPlaceholderAndLabelAndDisableShrink = Template.bind( 47 | {} 48 | ); 49 | NumberInputWithPlaceholderAndLabelAndDisableShrink.args = { 50 | title: 'NumberInput with placeholder and label and disable shrink', 51 | placeholder: 'Placeholder', 52 | label: 'Label', 53 | disableShrink: true, 54 | }; 55 | 56 | export const NumberInputDisabled = Template.bind({}); 57 | NumberInputDisabled.args = { 58 | title: 'Disabled NumberInput', 59 | placeholder: 'Placeholder', 60 | disabled: true, 61 | label: 'Label', 62 | }; 63 | 64 | export const NumberInputError = Template.bind({}); 65 | NumberInputError.args = { 66 | title: 'NumberInput with error', 67 | label: 'Label', 68 | error: 'Error Message', 69 | }; 70 | 71 | const Component = { 72 | title: 'Form/NumberInput', 73 | component: Index, 74 | }; 75 | 76 | export default Component; 77 | -------------------------------------------------------------------------------- /src/NumberInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import Input, { type InputProps } from '../Input'; 3 | import s from './NumberInput.module.scss'; 4 | 5 | interface NumberInputProps extends InputProps { 6 | name: string; 7 | value?: string | number; 8 | onChange: (event: { 9 | target: { name: string; value: string | number }; 10 | }) => void; 11 | } 12 | 13 | const NumberInput = forwardRef( 14 | ({ value, onChange, name, ...rest }, inputRef) => { 15 | const handleIncrease = () => { 16 | handleChange(parseInt(value as string) + 1); 17 | }; 18 | 19 | const handleDecrease = () => { 20 | handleChange(parseInt(value as string) - 1); 21 | }; 22 | 23 | const handleChange = (newValue: number) => { 24 | onChange({ 25 | target: { 26 | name, 27 | value: newValue, 28 | }, 29 | }); 30 | }; 31 | 32 | return ( 33 | 43 | 55 | 63 | 64 | } 65 | /> 66 | ); 67 | } 68 | ); 69 | 70 | NumberInput.displayName = 'NumberInput'; 71 | 72 | export default NumberInput; 73 | 74 | export { type NumberInputProps }; 75 | -------------------------------------------------------------------------------- /src/OTPInput/OTPInput.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-bottom: var(--rfe-spacing); 3 | width: 100%; 4 | display: flex; 5 | gap: 1rem; 6 | } 7 | 8 | .inputRoot { 9 | width: 100%; 10 | pointer-events: all; 11 | position: relative; 12 | display: flex; 13 | flex-direction: column; 14 | } 15 | 16 | .inputs { 17 | position: relative; 18 | display: flex; 19 | align-items: center; 20 | color: var(--rfe-black); 21 | gap: 1rem; 22 | } 23 | 24 | .label { 25 | font-size: var(--rfe-font-size); 26 | line-height: 1; 27 | color: var(--rfe-color); 28 | margin-bottom: var(--rfe-label-spacing); 29 | } 30 | 31 | .errorLabel { 32 | color: var(--rfe-color-error); 33 | font-size: var(--rfe-error-font-size); 34 | pointer-events: none; 35 | } 36 | 37 | .resendContainer { 38 | display: flex; 39 | justify-content: space-between; 40 | color: var(--rfe-black); 41 | margin-top: var(--rfe-label-spacing); 42 | } 43 | 44 | .resend { 45 | padding: 0; 46 | margin: 0; 47 | border: 0; 48 | background: none; 49 | font-size: var(--rfe-placeholder-font-size); 50 | line-height: 16px; 51 | color: var(--rfe-black); 52 | opacity: 0.5; 53 | pointer-events: none; 54 | } 55 | 56 | .resendActive { 57 | color: var(--rfe-black); 58 | font-weight: 500; 59 | text-decoration: underline; 60 | opacity: 1; 61 | pointer-events: all; 62 | cursor: pointer; 63 | } 64 | 65 | .time { 66 | font-size: var(--rfe-placeholder-font-size); 67 | line-height: 16px; 68 | text-align: right; 69 | color: var(--rfe-color-error); 70 | } 71 | -------------------------------------------------------------------------------- /src/OTPInput/OTPInput.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Index from './index'; 4 | 5 | const Template = (arg) => { 6 | const [inputs, setInputs] = useState({}); 7 | const onChange = (e) => { 8 | const { name, value, type } = e.target; 9 | 10 | setInputs((prevState) => ({ 11 | ...prevState, 12 | [name]: type === 'checkbox' ? !prevState[name] : value, 13 | })); 14 | }; 15 | 16 | return ( 17 | <> 18 |

{arg.title}

19 | 20 | 21 | ); 22 | }; 23 | 24 | export const OTPInputComponent = Template.bind({}); 25 | OTPInputComponent.args = { title: 'OTPInput', length: 5 }; 26 | 27 | export const OTPInputWithLabel = Template.bind({}); 28 | OTPInputWithLabel.args = { 29 | title: 'OTPInput with label', 30 | length: 5, 31 | label: 'Label', 32 | }; 33 | 34 | export const OTPInputWithLength = Template.bind({}); 35 | OTPInputWithLength.args = { 36 | label: 'Label', 37 | title: 'OTPInput with Length', 38 | length: 4, 39 | }; 40 | 41 | export const OTPInputWithFormat = Template.bind({}); 42 | OTPInputWithFormat.args = { 43 | title: 'OTPInput with Format', 44 | length: 6, 45 | format: [2, 2, 2], 46 | }; 47 | 48 | export const OTPInputWithSeparator = Template.bind({}); 49 | OTPInputWithSeparator.args = { 50 | title: 'OTPInput with Separator', 51 | length: 6, 52 | format: [3, 3], 53 | separator: '/', 54 | }; 55 | 56 | export const OTPInputWithTimer = Template.bind({}); 57 | OTPInputWithTimer.args = { 58 | title: 'OTPInput with Separator', 59 | length: 6, 60 | format: [3, 3], 61 | timer: 120, 62 | }; 63 | 64 | export const OTPInputWithOnComplete = Template.bind({}); 65 | OTPInputWithOnComplete.args = { 66 | title: 'OTPInput with OnComplete', 67 | length: 6, 68 | format: [3, 3], 69 | timer: 120, 70 | onComplete: () => { 71 | alert('completed'); 72 | }, 73 | }; 74 | 75 | export const OTPInputWithOnResend = Template.bind({}); 76 | OTPInputWithOnResend.args = { 77 | title: 'OTPInput with OnResend', 78 | length: 6, 79 | timer: 120, 80 | onResend: () => { 81 | alert('resend'); 82 | }, 83 | }; 84 | 85 | export const OTPInputDisabled = Template.bind({}); 86 | OTPInputDisabled.args = { 87 | title: 'Disabled OTPInput', 88 | placeholder: 'Placeholder', 89 | disabled: true, 90 | length: 5, 91 | label: 'Label', 92 | }; 93 | 94 | export const OTPInputErrorMessage = Template.bind({}); 95 | OTPInputErrorMessage.args = { 96 | title: 'OTPInput with error message', 97 | label: 'Label', 98 | length: 5, 99 | error: 'Error Message', 100 | }; 101 | 102 | export const OTPInputError = Template.bind({}); 103 | OTPInputError.args = { 104 | title: 'OTPInput with error', 105 | label: 'Label', 106 | length: 5, 107 | error: true, 108 | }; 109 | 110 | const Component = { 111 | title: 'Form/OTPInput', 112 | component: Index, 113 | }; 114 | 115 | export default Component; 116 | -------------------------------------------------------------------------------- /src/OTPInput/Slot/Slot.module.scss: -------------------------------------------------------------------------------- 1 | .slot { 2 | position: relative; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | width: 100%; 7 | font-size: var(--rfe-font-size); 8 | line-height: 24px; 9 | background-color: var(--rfe-background); 10 | border: 1px solid var(--rfe-border); 11 | border-radius: var(--rfe-border-radius); 12 | min-height: var(--rfe-input-height); 13 | transition: all 250ms; 14 | font-family: var(--rfe-font-family); 15 | color: var(--rfe-color); 16 | } 17 | 18 | .slotActive { 19 | box-shadow: var(--rfe-focus); 20 | } 21 | 22 | .slotDisabled { 23 | background: var(--rfe-background-disabled); 24 | color: var(--rfe-color); 25 | cursor: not-allowed; 26 | opacity: 1; 27 | } 28 | 29 | .slotError { 30 | border-color: var(--rfe-color-error) !important; 31 | } 32 | 33 | .caretRoot { 34 | position: absolute; 35 | pointer-events: none; 36 | inset: 0; 37 | display: flex; 38 | align-items: center; 39 | justify-content: center; 40 | width: 100%; 41 | } 42 | 43 | .caret { 44 | width: 1px; 45 | min-height: calc(var(--rfe-input-height) / 3); 46 | background-color: var(--rfe-black); 47 | animation: caret-blink 1.2s ease-out infinite; 48 | } 49 | 50 | @keyframes caret-blink { 51 | 0%, 52 | 70%, 53 | 100% { 54 | opacity: 1; 55 | } 56 | 57 | 20%, 58 | 50% { 59 | opacity: 0; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/OTPInput/Slot/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import s from './Slot.module.scss'; 3 | import cx from 'clsx'; 4 | 5 | interface SlotProps { 6 | char?: string | null; 7 | hasFakeCaret: boolean; 8 | isActive: boolean; 9 | disabled?: boolean; 10 | inputClassName?: string; 11 | error?: boolean; 12 | } 13 | 14 | const Slot: React.FC = ({ 15 | char, 16 | hasFakeCaret, 17 | isActive, 18 | disabled, 19 | inputClassName, 20 | error = false, 21 | }) => { 22 | return ( 23 |
31 | {char !== null &&
{char}
} 32 | {hasFakeCaret ? ( 33 |
34 |
35 |
36 | ) : null} 37 |
38 | ); 39 | }; 40 | 41 | export default Slot; 42 | -------------------------------------------------------------------------------- /src/OTPInput/index.tsx: -------------------------------------------------------------------------------- 1 | import { OTPInput } from 'input-otp'; 2 | import React, { forwardRef, useCallback, useEffect, useMemo } from 'react'; 3 | import s from './OTPInput.module.scss'; 4 | import Slot from './Slot'; 5 | import cx from 'clsx'; 6 | import { useCountDown } from '../utils/useCountDown'; 7 | import { remove, set } from '../utils/localStorage'; 8 | import formatSeconds from '../utils/date'; 9 | import { FieldError } from 'react-hook-form'; 10 | 11 | interface OTPInputProps { 12 | name: string; 13 | length: number; 14 | onChange: (event: { target: { name: string; value: string } }) => void; 15 | format?: number[] | null; 16 | separator?: string | React.ReactElement | null; 17 | timer?: number | null; 18 | onComplete?: () => void; 19 | onResend?: () => void; 20 | value?: string; 21 | error?: 22 | | boolean 23 | | string 24 | | { message?: string } 25 | | null 26 | | undefined 27 | | FieldError; 28 | label?: string | null; 29 | resendLabel?: string; 30 | inputClassName?: string; 31 | labelClassName?: string; 32 | errorClassName?: string; 33 | disabled?: boolean; 34 | } 35 | 36 | const OTPInputComponent = forwardRef( 37 | ( 38 | { 39 | name, 40 | length, 41 | onChange, 42 | format = null, 43 | separator = '-', 44 | timer = null, 45 | onComplete = () => ({}), 46 | onResend = () => ({}), 47 | value = '', 48 | error = null, 49 | label = null, 50 | resendLabel = 'Resend', 51 | inputClassName = null, 52 | labelClassName = null, 53 | errorClassName = null, 54 | disabled = false, 55 | }, 56 | inputRef 57 | ) => { 58 | const [seconds, resetSeconds] = useCountDown(name); 59 | 60 | const handleResend = useCallback(() => { 61 | remove(name); 62 | remove(`${name}_TIMER`); 63 | resetSeconds(); 64 | 65 | onChange({ 66 | target: { 67 | name: name, 68 | value: '', 69 | }, 70 | }); 71 | onResend(); 72 | }, [onResend, resetSeconds, name, onChange]); 73 | 74 | useEffect(() => { 75 | if (!timer) return; 76 | 77 | set(name, seconds); 78 | set(`${name}_TIMER`, +new Date()); 79 | }, [name, seconds, timer]); 80 | 81 | const errorMessage = useMemo(() => { 82 | if (typeof error === 'string') { 83 | return error; 84 | } else if (typeof error === 'object' && error?.message) { 85 | return error.message; 86 | } 87 | return null; 88 | }, [error]); 89 | 90 | const labelEl = useMemo( 91 | () => ( 92 | 100 | ), 101 | [name, label, labelClassName] 102 | ); 103 | 104 | const handleChange = useCallback( 105 | (e: string) => { 106 | onChange({ 107 | target: { 108 | name: name, 109 | value: e, 110 | }, 111 | }); 112 | }, 113 | [onChange, name] 114 | ); 115 | 116 | const createSlot = (slot: any, idx: string | number) => ( 117 | 126 | ); 127 | 128 | const generateSlots = (format: number[], slots: any[]) => { 129 | const allSlots: React.ReactNode[] = []; 130 | const cumulativeSums = format.reduce((acc, curr, index) => { 131 | const sum = index === 0 ? curr : acc[index - 1]! + curr; 132 | acc.push(sum); 133 | return acc; 134 | }, [] as number[]); 135 | 136 | format.forEach((_, index) => { 137 | const start = index === 0 ? 0 : cumulativeSums[index - 1]; 138 | const end = cumulativeSums[index]; 139 | 140 | const slotComponents = slots 141 | .slice(start, end) 142 | .map((slot, idx) => createSlot(slot, `${idx}_${index}`)); 143 | 144 | allSlots.push(...slotComponents); 145 | if (separator && end !== slots.length) allSlots.push(separator); 146 | }); 147 | 148 | return allSlots; 149 | }; 150 | 151 | return ( 152 |
153 |
154 | {label && labelEl} 155 | ( 163 |
164 | {format ? generateSlots(format, slots) : slots.map(createSlot)} 165 |
166 | )} 167 | /> 168 |
169 |
170 | {errorMessage && ( 171 | 176 | {errorMessage} 177 | 178 | )} 179 |
180 | {timer && ( 181 | <> 182 | {seconds === 0 ? ( 183 | 193 | ) : ( 194 |
{formatSeconds(seconds)}
195 | )} 196 | 197 | )} 198 |
199 |
200 |
201 | ); 202 | } 203 | ); 204 | 205 | OTPInputComponent.displayName = 'OTPInput'; 206 | 207 | export default OTPInputComponent; 208 | 209 | export { type OTPInputProps }; 210 | -------------------------------------------------------------------------------- /src/PasswordInput/PasswordInput.module.scss: -------------------------------------------------------------------------------- 1 | .toggle { 2 | background: none; 3 | border: 0; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | pointer-events: all; 8 | cursor: pointer; 9 | } 10 | 11 | .iconEye { 12 | color: var(--rfe-color); 13 | padding: 16px 0; 14 | } 15 | 16 | .eyeOpen button::after { 17 | content: 'Close'; 18 | } 19 | 20 | .eyeClose button::after { 21 | content: 'Open'; 22 | } 23 | 24 | .eye { 25 | --duration-blink: 0.2s; 26 | --duration-lashes: 0.2s; 27 | --delay-lashes: var(--duration-blink); 28 | --duration-pupil: 0.1s; 29 | --delay-pupil: calc(var(--duration-blink) * 2 / 3); 30 | } 31 | 32 | .eyeBottom, 33 | .eyeTop { 34 | stroke-linecap: round; 35 | } 36 | 37 | .eyeTop, 38 | .eyeLashes { 39 | transition: var(--duration-blink) ease-in; 40 | } 41 | 42 | .eyePupil { 43 | opacity: 0; 44 | transition: opacity var(--duration-pupil) var(--delay-pupil) ease; 45 | } 46 | 47 | .eyeOpen .eyeTop, 48 | .eyeOpen .eyeLashes { 49 | transform: rotateX(0.5turn); 50 | animation: scale-up var(--duration-lashes) var(--delay-lashes) ease-in-out; 51 | } 52 | 53 | .eyeOpen .eyePupil { 54 | opacity: 1; 55 | } 56 | 57 | .eyeClose .eyeLashes { 58 | animation: scale-down var(--duration-lashes) var(--duration-blink) ease-in-out; 59 | } 60 | 61 | .eyeClose .eyePupil { 62 | opacity: 0; 63 | } 64 | 65 | @keyframes scale-up { 66 | 50% { 67 | transform: rotateX(0.5turn) scaleY(1.15); 68 | } 69 | 70 | 100% { 71 | transform: rotateX(0.5turn) scaleY(1); 72 | } 73 | } 74 | 75 | @keyframes scale-down { 76 | 50% { 77 | transform: scaleY(1.15); 78 | } 79 | 80 | 100% { 81 | transform: scaleY(1); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/PasswordInput/PasswordInput.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Index from './index'; 4 | 5 | const Template = (arg) => { 6 | const [inputs, setInputs] = useState({}); 7 | const onChange = (e) => { 8 | const { name, value, type } = e.target; 9 | 10 | setInputs((prevState) => ({ 11 | ...prevState, 12 | [name]: type === 'checkbox' ? !prevState[name] : value, 13 | })); 14 | }; 15 | 16 | return ( 17 | <> 18 |

{arg.title}

19 | 20 | 21 | ); 22 | }; 23 | 24 | export const PasswordInputComponent = Template.bind({}); 25 | PasswordInputComponent.args = { title: 'PasswordInput' }; 26 | 27 | export const PasswordInputWithLabel = Template.bind({}); 28 | PasswordInputWithLabel.args = { 29 | title: 'PasswordInput with label', 30 | label: 'Label', 31 | }; 32 | 33 | export const PasswordInputWithPlaceholder = Template.bind({}); 34 | PasswordInputWithPlaceholder.args = { 35 | title: 'PasswordInput with placeholder', 36 | placeholder: 'Placeholder', 37 | }; 38 | 39 | export const PasswordInputWithPlaceholderAndLabel = Template.bind({}); 40 | PasswordInputWithPlaceholderAndLabel.args = { 41 | title: 'PasswordInput with placeholder and label', 42 | placeholder: 'Placeholder', 43 | label: 'Label', 44 | }; 45 | 46 | export const PasswordInputWithPlaceholderAndLabelAndDisableShrink = 47 | Template.bind({}); 48 | PasswordInputWithPlaceholderAndLabelAndDisableShrink.args = { 49 | title: 'PasswordInput with placeholder and label and disable shrink', 50 | placeholder: 'Placeholder', 51 | label: 'Label', 52 | disableShrink: true, 53 | }; 54 | 55 | export const PasswordInputDisabled = Template.bind({}); 56 | PasswordInputDisabled.args = { 57 | title: 'Disabled PasswordInput', 58 | placeholder: 'Placeholder', 59 | disabled: true, 60 | label: 'Label', 61 | }; 62 | 63 | export const PasswordInputError = Template.bind({}); 64 | PasswordInputError.args = { 65 | title: 'PasswordInput with error', 66 | label: 'Label', 67 | error: 'Error Message', 68 | }; 69 | 70 | const Component = { 71 | title: 'Form/PasswordInput', 72 | component: Index, 73 | }; 74 | 75 | export default Component; 76 | -------------------------------------------------------------------------------- /src/PasswordInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useCallback, useState } from 'react'; 2 | import cx from 'clsx'; 3 | import Input, { type InputProps } from '../Input'; 4 | import s from './PasswordInput.module.scss'; 5 | 6 | type PasswordInputProps = InputProps; 7 | 8 | const PasswordInput = forwardRef( 9 | ({ ...rest }, inputRef) => { 10 | const [isOpen, setIsOpen] = useState(false); 11 | 12 | const handleToggle = useCallback( 13 | (e: React.MouseEvent) => { 14 | e.preventDefault(); 15 | setIsOpen((prev) => !prev); 16 | }, 17 | [] 18 | ); 19 | 20 | return ( 21 | 27 |
33 |
34 | 40 | 41 | 42 | 43 | 44 | 49 | 54 | 60 | 61 | 62 |
63 |
64 | 65 | } 66 | /> 67 | ); 68 | } 69 | ); 70 | 71 | PasswordInput.displayName = 'PasswordInput'; 72 | 73 | export default PasswordInput; 74 | 75 | export { type PasswordInputProps }; 76 | -------------------------------------------------------------------------------- /src/PhoneInput/PhoneInput.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-bottom: 16px; 3 | width: 100%; 4 | } 5 | 6 | .prepend { 7 | padding: 10px; 8 | 9 | & svg { 10 | width: 24px; 11 | height: 24px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/PhoneInput/PhoneInput.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { PieIcon } from '../Icon'; 4 | import Index from './index'; 5 | 6 | const Template = (arg) => { 7 | const [inputs, setInputs] = useState({}); 8 | const onChange = (e) => { 9 | const { name, value, type } = e.target; 10 | 11 | setInputs((prevState) => ({ 12 | ...prevState, 13 | [name]: type === 'checkbox' ? !prevState[name] : value, 14 | })); 15 | }; 16 | 17 | return ( 18 | <> 19 |

{arg.title}

20 | 21 | 22 | ); 23 | }; 24 | export const PhoneInputComponent = Template.bind({}); 25 | PhoneInputComponent.args = { title: 'PhoneInput' }; 26 | 27 | export const PhoneInputWithLabel = Template.bind({}); 28 | PhoneInputWithLabel.args = { title: 'PhoneInput with label', label: 'Label' }; 29 | 30 | export const PhoneInputWithPlaceholder = Template.bind({}); 31 | PhoneInputWithPlaceholder.args = { 32 | title: 'PhoneInput with placeholder', 33 | placeholder: '+1 234 567 8901', 34 | }; 35 | 36 | export const PhoneInputWithPlaceholderAndLabel = Template.bind({}); 37 | PhoneInputWithPlaceholderAndLabel.args = { 38 | title: 'PhoneInput with placeholder and label', 39 | placeholder: '+1 234 567 8901', 40 | label: 'Label', 41 | }; 42 | 43 | export const PhoneInputWithPlaceholderAndLabelAndDisableShrink = Template.bind( 44 | {} 45 | ); 46 | PhoneInputWithPlaceholderAndLabelAndDisableShrink.args = { 47 | title: 'PhoneInput with placeholder and label and disable shrink', 48 | placeholder: '+1 234 567 8901', 49 | label: 'Label', 50 | disableShrink: true, 51 | }; 52 | 53 | export const PhoneInputDisabled = Template.bind({}); 54 | PhoneInputDisabled.args = { 55 | title: 'Disabled PhoneInput', 56 | placeholder: '+1 234 567 8901', 57 | disabled: true, 58 | label: 'Label', 59 | }; 60 | 61 | export const PhoneInputPrepend = Template.bind({}); 62 | PhoneInputPrepend.args = { 63 | title: 'PhoneInput with prepend', 64 | placeholder: '+1 234 567 8901', 65 | label: 'Label', 66 | prepend: , 67 | }; 68 | 69 | export const PhoneInputError = Template.bind({}); 70 | PhoneInputError.args = { 71 | title: 'PhoneInput with error', 72 | label: 'Label', 73 | error: 'Error Message', 74 | }; 75 | 76 | const Component = { 77 | title: 'Form/PhoneInput', 78 | component: Index, 79 | }; 80 | 81 | export default Component; 82 | -------------------------------------------------------------------------------- /src/PhoneInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import Input, { type InputProps } from '../Input'; 3 | import s from './PhoneInput.module.scss'; 4 | import { usePhoneInput, FlagImage } from 'react-international-phone'; 5 | 6 | interface PhoneInputProps extends InputProps { 7 | name: string; 8 | onChange: (event: React.ChangeEvent) => void; 9 | value?: string | number; 10 | defaultCountry?: string; 11 | [key: string]: any; 12 | } 13 | 14 | const PhoneInput = forwardRef( 15 | ( 16 | { name, onChange, value = '', defaultCountry = 'us', ...rest }, 17 | inputRef 18 | ) => { 19 | const phoneInput = usePhoneInput({ 20 | defaultCountry, 21 | value, 22 | onChange: ({ phone }) => { 23 | onChange({ 24 | target: { 25 | name, 26 | value: phone, 27 | }, 28 | }); 29 | }, 30 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 31 | // @ts-expect-error 32 | inputRef, 33 | }); 34 | 35 | return ( 36 |
37 | } 45 | appendClassName={s.prepend} 46 | /> 47 |
48 | ); 49 | } 50 | ); 51 | 52 | PhoneInput.displayName = 'PhoneInput'; 53 | 54 | export default PhoneInput; 55 | 56 | export { type PhoneInputProps }; 57 | -------------------------------------------------------------------------------- /src/Radio/Radio.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | display: inline-flex; 4 | flex-direction: column; 5 | margin-bottom: var(--rfe-spacing); 6 | } 7 | 8 | .inputRoot { 9 | position: relative; 10 | display: flex; 11 | align-items: center; 12 | justify-content: flex-start; 13 | } 14 | 15 | .label { 16 | margin-top: 1px; 17 | font-size: var(--rfe-font-size); 18 | line-height: 20px; 19 | color: var(--rfe-color); 20 | pointer-events: all; 21 | cursor: pointer; 22 | } 23 | 24 | .box { 25 | width: 28px; 26 | height: 28px; 27 | border: solid 1px var(--rfe-border); 28 | border-radius: 24px; 29 | margin-right: 16px; 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | color: var(--rfe-white); 34 | flex-shrink: 0; 35 | background-color: var(--rfe-background); 36 | transition: all 250ms; 37 | 38 | &:hover { 39 | box-shadow: var(--rfe-focus); 40 | } 41 | } 42 | 43 | .noLabel { 44 | margin: 0; 45 | } 46 | 47 | .boxDisabled { 48 | background-color: var(--rfe-background-disabled); 49 | 50 | &:hover { 51 | box-shadow: none; 52 | cursor: not-allowed; 53 | } 54 | } 55 | 56 | .inputError { 57 | border-color: var(--rfe-color-error) !important; 58 | } 59 | 60 | .icon { 61 | display: none; 62 | line-height: 1; 63 | color: var(--rfe-color); 64 | } 65 | 66 | .input { 67 | position: fixed; 68 | top: 0; 69 | left: 0; 70 | z-index: 0; 71 | opacity: 0; 72 | width: 0; 73 | height: 0; 74 | } 75 | 76 | .input:checked ~ .box { 77 | background-color: var(--rfe-background-selected); 78 | border-color: var(--rfe-border); 79 | } 80 | 81 | .input:checked ~ .boxDisabled { 82 | background: var(--rfe-background-disabled) !important; 83 | } 84 | 85 | .input:checked ~ .box .icon { 86 | display: block; 87 | } 88 | 89 | .input:focus-visible ~ .box { 90 | box-shadow: var(--rfe-focus); 91 | } 92 | 93 | .errorLabel { 94 | color: var(--rfe-color-error); 95 | font-size: var(--rfe-error-font-size); 96 | margin-top: var(--rfe-label-spacing); 97 | pointer-events: none; 98 | } 99 | -------------------------------------------------------------------------------- /src/Radio/Radio.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Index from './index'; 4 | 5 | const Template = (args) => { 6 | const [inputs, setInputs] = useState({}); 7 | const onChange = (e) => { 8 | const { name, value, type } = e.target; 9 | 10 | setInputs((prevState) => ({ 11 | ...prevState, 12 | [name]: type === 'checkbox' ? !prevState[name] : value, 13 | })); 14 | }; 15 | 16 | return ( 17 | <> 18 |

{args.title}

19 |
26 | 34 | 42 |
43 | 44 | ); 45 | }; 46 | 47 | export const RadioComponent = Template.bind({}); 48 | RadioComponent.args = { title: 'Radio' }; 49 | 50 | export const RadioComponentChecked = Template.bind({}); 51 | RadioComponentChecked.args = { title: 'Radio', checked: true }; 52 | 53 | export const RadioComponentDisabled = Template.bind({}); 54 | RadioComponentDisabled.args = { title: 'Radio', disabled: true }; 55 | 56 | export const RadioComponentError = Template.bind({}); 57 | RadioComponentError.args = { 58 | title: 'Radio with error', 59 | error: 'Error Message', 60 | }; 61 | 62 | const Component = { 63 | title: 'Form/Radio', 64 | component: RadioComponent, 65 | }; 66 | 67 | export default Component; 68 | -------------------------------------------------------------------------------- /src/Radio/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useMemo } from 'react'; 2 | import cx from 'clsx'; 3 | import { CheckIcon } from '../Icon'; 4 | import s from './Radio.module.scss'; 5 | import { FieldError } from 'react-hook-form'; 6 | 7 | interface RadioProps { 8 | name: string; 9 | onChange: (event: React.ChangeEvent) => void; 10 | value: string; 11 | label?: string | React.ReactNode; 12 | checked?: boolean; 13 | inputClassName?: string; 14 | labelClassName?: string; 15 | errorClassName?: string; 16 | disabled?: boolean; 17 | error?: 18 | | boolean 19 | | string 20 | | { message?: string } 21 | | null 22 | | undefined 23 | | FieldError; 24 | [rest: string]: any; 25 | } 26 | 27 | const Radio = forwardRef( 28 | ( 29 | { 30 | name, 31 | onChange, 32 | value, 33 | label = null, 34 | checked = false, 35 | inputClassName = null, 36 | labelClassName = null, 37 | errorClassName = null, 38 | disabled = false, 39 | error = null, 40 | ...rest 41 | }, 42 | inputRef 43 | ) => { 44 | const errorMessage = useMemo(() => { 45 | if (error && typeof error === 'string') { 46 | return error; 47 | } else if (error && typeof error === 'object' && error.message) { 48 | return error.message; 49 | } 50 | return null; 51 | }, [error]); 52 | 53 | return ( 54 | 96 | ); 97 | } 98 | ); 99 | 100 | Radio.displayName = 'Radio'; 101 | 102 | export default Radio; 103 | 104 | export { type RadioProps }; 105 | -------------------------------------------------------------------------------- /src/RadioCards/RadioCards.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | pointer-events: all; 3 | position: relative; 4 | display: flex; 5 | flex-direction: column; 6 | margin-bottom: var(--rfe-spacing); 7 | 8 | & label { 9 | margin-bottom: var(--rfe-label-spacing); 10 | } 11 | 12 | & label:last-of-type { 13 | margin-bottom: 0; 14 | } 15 | } 16 | 17 | .label { 18 | font-size: var(--rfe-font-size); 19 | line-height: 1; 20 | color: var(--rfe-color); 21 | margin-bottom: var(--rfe-label-spacing); 22 | } 23 | 24 | .errorLabel { 25 | color: var(--rfe-color-error); 26 | font-size: var(--rfe-error-font-size); 27 | margin-top: var(--rfe-label-spacing); 28 | } 29 | 30 | .container { 31 | display: flex; 32 | flex-wrap: wrap; 33 | gap: var(--rfe-spacing); 34 | } 35 | 36 | .row { 37 | flex-direction: row; 38 | } 39 | 40 | .column { 41 | flex-direction: column; 42 | } 43 | 44 | .card { 45 | cursor: pointer; 46 | border: solid 1px var(--rfe-border); 47 | border-radius: var(--rfe-border-radius); 48 | display: flex; 49 | padding: 0; 50 | align-items: center; 51 | justify-content: center; 52 | color: var(--rfe-white); 53 | flex-shrink: 0; 54 | background-color: var(--rfe-background); 55 | transition: all 250ms; 56 | 57 | label { 58 | width: 100%; 59 | text-align: center; 60 | padding: var(--rfe-label-spacing); 61 | 62 | div { 63 | width: 100%; 64 | text-align: center; 65 | } 66 | } 67 | } 68 | 69 | .card:focus-within { 70 | box-shadow: var(--rfe-focus); 71 | } 72 | 73 | .card:hover { 74 | box-shadow: var(--rfe-focus); 75 | } 76 | 77 | .selected { 78 | background-color: var(--rfe-background-selected); 79 | border-color: var(--rfe-border); 80 | } 81 | 82 | .disabled { 83 | background-color: var(--rfe-background-disabled); 84 | box-shadow: none !important; 85 | 86 | & > label:hover { 87 | box-shadow: none; 88 | cursor: not-allowed; 89 | 90 | div { 91 | cursor: not-allowed; 92 | } 93 | } 94 | } 95 | 96 | .hide { 97 | display: none; 98 | } 99 | 100 | .inputError { 101 | border-color: var(--rfe-color-error) !important; 102 | } 103 | -------------------------------------------------------------------------------- /src/RadioCards/RadioCards.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Index from './index'; 4 | 5 | const Template = (args) => { 6 | const [inputs, setInputs] = useState({}); 7 | const onChange = (e) => { 8 | const { name, value, type } = e.target; 9 | 10 | setInputs((prevState) => ({ 11 | ...prevState, 12 | [name]: type === 'checkbox' ? !prevState[name] : value, 13 | })); 14 | }; 15 | 16 | return ( 17 | <> 18 |

{args.title}

19 |
26 | 37 |
38 | 39 | ); 40 | }; 41 | 42 | export const RadioCardsComponent = Template.bind({}); 43 | RadioCardsComponent.args = { title: 'RadioCards' }; 44 | 45 | export const RadioCardsComponentChecked = Template.bind({}); 46 | RadioCardsComponentChecked.args = { title: 'RadioCards', checked: true }; 47 | 48 | export const RadioCardsComponentRow = Template.bind({}); 49 | RadioCardsComponentRow.args = { 50 | title: 'RadioCards', 51 | direction: 'row', 52 | }; 53 | 54 | export const RadioCardsComponentWithInput = Template.bind({}); 55 | RadioCardsComponentWithInput.args = { 56 | title: 'RadioCards', 57 | hideInput: false, 58 | }; 59 | 60 | export const RadioCardsComponentDisabled = Template.bind({}); 61 | RadioCardsComponentDisabled.args = { title: 'RadioCards', disabled: true }; 62 | 63 | export const RadioCardsComponentError = Template.bind({}); 64 | RadioCardsComponentError.args = { 65 | title: 'RadioCards with error', 66 | error: 'Error Message', 67 | }; 68 | 69 | const Component = { 70 | title: 'Form/RadioCards', 71 | component: RadioCardsComponent, 72 | }; 73 | 74 | export default Component; 75 | -------------------------------------------------------------------------------- /src/RadioCards/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useMemo } from 'react'; 2 | import cx from 'clsx'; 3 | import Radio from '../Radio'; 4 | import s from './RadioCards.module.scss'; 5 | import { FieldError } from 'react-hook-form'; 6 | 7 | interface Option { 8 | value: string; 9 | label: string | React.ReactNode; 10 | } 11 | 12 | interface RadioCardsProps { 13 | name: string; 14 | onChange: (event: React.ChangeEvent) => void; 15 | value?: string | null; 16 | options: Option[]; 17 | label?: string | React.ReactNode; 18 | inputClassName?: string; 19 | cardClassName?: string; 20 | labelClassName?: string; 21 | optionLabelClassName?: string; 22 | direction?: 'row' | 'column'; 23 | errorClassName?: string; 24 | disabled?: boolean; 25 | hideInput?: boolean; 26 | error?: 27 | | boolean 28 | | string 29 | | { message?: string } 30 | | null 31 | | undefined 32 | | FieldError; 33 | } 34 | 35 | const RadioCards = forwardRef( 36 | ( 37 | { 38 | name, 39 | onChange, 40 | value = null, 41 | options = [], 42 | label = null, 43 | inputClassName = null, 44 | cardClassName = null, 45 | labelClassName = null, 46 | optionLabelClassName = null, 47 | direction = 'column', 48 | errorClassName = null, 49 | disabled = false, 50 | hideInput = true, 51 | error = null, 52 | }, 53 | inputRef 54 | ) => { 55 | const errorMessage = useMemo(() => { 56 | if (error && typeof error === 'string') return error; 57 | if (error && typeof error === 'object' && error.message) 58 | return error.message; 59 | return null; 60 | }, [error]); 61 | 62 | const handleClick = (optionValue: string) => { 63 | if (disabled) return; 64 | 65 | onChange({ 66 | target: { 67 | name, 68 | value: optionValue, 69 | }, 70 | } as React.ChangeEvent); 71 | }; 72 | 73 | return ( 74 | 125 | ); 126 | } 127 | ); 128 | 129 | RadioCards.displayName = 'RadioCards'; 130 | 131 | export default RadioCards; 132 | 133 | export { type RadioCardsProps }; 134 | -------------------------------------------------------------------------------- /src/RadioGroup/RadioGroup.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | pointer-events: all; 3 | position: relative; 4 | display: flex; 5 | flex-direction: column; 6 | margin-bottom: var(--rfe-spacing); 7 | 8 | & label { 9 | margin-bottom: var(--rfe-label-spacing); 10 | } 11 | 12 | & label:last-of-type { 13 | margin-bottom: 0; 14 | } 15 | } 16 | 17 | .label { 18 | font-size: var(--rfe-font-size); 19 | line-height: 1; 20 | color: var(--rfe-color); 21 | margin-bottom: var(--rfe-label-spacing); 22 | } 23 | 24 | .errorLabel { 25 | color: var(--rfe-color-error); 26 | font-size: var(--rfe-error-font-size); 27 | margin-top: var(--rfe-label-spacing); 28 | } 29 | -------------------------------------------------------------------------------- /src/RadioGroup/RadioGroup.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Index from './index'; 4 | 5 | const Template = (args) => { 6 | const [inputs, setInputs] = useState({}); 7 | const onChange = (e) => { 8 | const { name, value, type } = e.target; 9 | 10 | setInputs((prevState) => ({ 11 | ...prevState, 12 | [name]: type === 'checkbox' ? !prevState[name] : value, 13 | })); 14 | }; 15 | 16 | return ( 17 | <> 18 |

{args.title}

19 |
26 | 37 |
38 | 39 | ); 40 | }; 41 | 42 | export const RadioGroupComponent = Template.bind({}); 43 | RadioGroupComponent.args = { title: 'RadioGroup' }; 44 | 45 | export const RadioGroupComponentChecked = Template.bind({}); 46 | RadioGroupComponentChecked.args = { title: 'RadioGroup', checked: true }; 47 | 48 | export const RadioGroupComponentDisabled = Template.bind({}); 49 | RadioGroupComponentDisabled.args = { title: 'RadioGroup', disabled: true }; 50 | 51 | export const RadioGroupComponentError = Template.bind({}); 52 | RadioGroupComponentError.args = { 53 | title: 'RadioGroup with error', 54 | error: 'Error Message', 55 | }; 56 | 57 | const Component = { 58 | title: 'Form/RadioGroup', 59 | component: RadioGroupComponent, 60 | }; 61 | 62 | export default Component; 63 | -------------------------------------------------------------------------------- /src/RadioGroup/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useMemo } from 'react'; 2 | import cx from 'clsx'; 3 | import Radio from '../Radio'; 4 | import s from './RadioGroup.module.scss'; 5 | import { FieldError } from 'react-hook-form'; 6 | 7 | interface Option { 8 | value: string; 9 | label: string | React.ReactNode; 10 | } 11 | 12 | interface RadioGroupProps { 13 | name: string; 14 | onChange: (event: React.ChangeEvent) => void; 15 | value?: string | null; 16 | options: Option[]; 17 | label?: string | React.ReactNode; 18 | inputClassName?: string; 19 | labelClassName?: string; 20 | optionLabelClassName?: string; 21 | errorClassName?: string; 22 | disabled?: boolean; 23 | error?: 24 | | boolean 25 | | string 26 | | { message?: string } 27 | | null 28 | | undefined 29 | | FieldError; 30 | } 31 | 32 | const RadioGroup = forwardRef( 33 | ( 34 | { 35 | name, 36 | onChange, 37 | value = null, 38 | options = [], 39 | label = null, 40 | inputClassName = null, 41 | labelClassName = null, 42 | optionLabelClassName = null, 43 | errorClassName = null, 44 | disabled = false, 45 | error = null, 46 | }, 47 | inputRef 48 | ) => { 49 | const errorMessage = useMemo(() => { 50 | if (error && typeof error === 'string') return error; 51 | if (error && typeof error === 'object' && error.message) 52 | return error.message; 53 | return null; 54 | }, [error]); 55 | 56 | return ( 57 | 90 | ); 91 | } 92 | ); 93 | 94 | RadioGroup.displayName = 'RadioGroup'; 95 | 96 | export default RadioGroup; 97 | 98 | export { type RadioGroupProps }; 99 | -------------------------------------------------------------------------------- /src/Select/Select.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-bottom: var(--rfe-spacing); 3 | position: relative; 4 | } 5 | 6 | .inputRoot { 7 | position: relative; 8 | } 9 | 10 | .label { 11 | line-height: 1; 12 | font-size: var(--rfe-font-size); 13 | color: var(--rfe-color); 14 | margin-bottom: 0; 15 | position: absolute; 16 | left: 16px; 17 | top: -2px; 18 | cursor: text; 19 | transform: translateY(calc((var(--rfe-input-height) - 14px) / 2)); 20 | transition: all 250ms; 21 | } 22 | 23 | .label.disableShrink { 24 | display: block; 25 | position: static; 26 | margin-bottom: var(--rfe-label-spacing); 27 | color: var(--rfe-color); 28 | transform: none; 29 | } 30 | 31 | .disabledLabel { 32 | pointer-events: none; 33 | cursor: not-allowed; 34 | } 35 | 36 | .labelPlaceholder { 37 | font-size: var(--rfe-focused-font-size); 38 | transform: translateY(9px); 39 | top: 0; 40 | } 41 | 42 | .labelFocused { 43 | font-size: var(--rfe-focused-font-size); 44 | transform: translateY(9px); 45 | top: 0; 46 | } 47 | 48 | .input { 49 | width: 100%; 50 | line-height: 24px; 51 | background-color: var(--rfe-background) !important; 52 | border: 1px solid var(--rfe-border) !important; 53 | border-radius: var(--rfe-border-radius); 54 | min-height: var(--rfe-input-height) !important; 55 | transition: all 250ms !important; 56 | font-family: var(--rfe-font-family); 57 | 58 | &:hover { 59 | border: 1px solid var(--rfe-border); 60 | } 61 | } 62 | 63 | .focus { 64 | box-shadow: var(--rfe-focus) !important; 65 | } 66 | 67 | .inputError { 68 | border-color: var(--rfe-color-error) !important; 69 | } 70 | 71 | .input:-webkit-autofill:not(.disableShrink) ~ .label, 72 | .input:focus:not(.disableShrink) ~ .label, 73 | .input:not([value=''], .disableShrink) ~ .label { 74 | font-size: var(--rfe-focused-font-size); 75 | transform: translateY(9px); 76 | } 77 | 78 | .input.disableShrink { 79 | padding: 16px; 80 | } 81 | 82 | .errorLabel { 83 | color: var(--rfe-color-error); 84 | font-size: var(--rfe-error-font-size); 85 | margin-top: var(--rfe-label-spacing); 86 | pointer-events: none; 87 | } 88 | 89 | .disabled { 90 | background: var(--rfe-background-disabled) !important; 91 | color: var(--rfe-color) !important; 92 | cursor: not-allowed !important; 93 | pointer-events: all; 94 | } 95 | 96 | .innerInput { 97 | margin-left: 8px !important; 98 | color: var(--rfe-color) !important; 99 | } 100 | 101 | .control { 102 | align-items: flex-end !important; 103 | } 104 | 105 | .menu { 106 | background-color: var(--rfe-background) !important; 107 | border: 1px solid var(--rfe-border); 108 | border-radius: var(--rfe-border-radius); 109 | padding: 0 !important; 110 | box-shadow: none !important; 111 | margin: 6px 0 0 !important; 112 | z-index: 9999; 113 | } 114 | 115 | .option { 116 | color: var(--rfe-color) !important; 117 | margin-left: 4px !important; 118 | margin-right: 4px !important; 119 | margin-bottom: 4px !important; 120 | width: calc(100% - 8px) !important; 121 | border-radius: calc(var(--rfe-border-radius) - 2px) !important; 122 | 123 | &:hover, 124 | &:focus, 125 | &:active, 126 | &:focus-visible, 127 | &:focus-within { 128 | background-color: var(--rfe-background-selected) !important; 129 | } 130 | 131 | &:last-child { 132 | margin-bottom: 0 !important; 133 | } 134 | } 135 | 136 | .optionFocused { 137 | color: var(--rfe-color) !important; 138 | background-color: var(--rfe-background-selected) !important; 139 | width: calc(100% - 8px) !important; 140 | margin-left: 4px !important; 141 | margin-right: 4px !important; 142 | margin-bottom: 4px !important; 143 | border-radius: calc(var(--rfe-border-radius) - 2px) !important; 144 | 145 | &:last-child { 146 | margin-bottom: 0 !important; 147 | } 148 | } 149 | 150 | .singleValue { 151 | color: var(--rfe-color) !important; 152 | margin-left: 8px !important; 153 | } 154 | 155 | .placeholder { 156 | color: var(--rfe-color-placeholder) !important; 157 | font-size: var(--rfe-placeholder-font-size); 158 | margin-left: 8px !important; 159 | } 160 | 161 | .indicatorSeparator { 162 | display: none; 163 | } 164 | 165 | .indicatorsContainer { 166 | min-width: 36px; 167 | display: flex; 168 | justify-content: center; 169 | } 170 | 171 | .indicatorsContainer, 172 | .clearIndicator, 173 | .multiValueRemove { 174 | & svg { 175 | color: var(--rfe-color); 176 | pointer-events: all; 177 | cursor: pointer; 178 | } 179 | } 180 | 181 | .multiValue { 182 | height: 24px; 183 | border-radius: var(--rfe-border-radius) !important; 184 | background-color: var(--rfe-background-selected) !important; 185 | align-items: center; 186 | 187 | & div { 188 | color: var(--rfe-color) !important; 189 | } 190 | } 191 | 192 | .valueContainer { 193 | padding-top: 20px !important; 194 | } 195 | 196 | .multiValueRemove { 197 | color: var(--rfe-color) !important; 198 | 199 | &:hover { 200 | background: unset !important; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Select/Select.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Index from './index'; 3 | 4 | const OPTIONS = [ 5 | { 6 | label: 'Item 1', 7 | value: 'item_1', 8 | }, 9 | { 10 | label: 'Item 2', 11 | value: 'item_2', 12 | }, 13 | { 14 | label: 'Item 3', 15 | value: 'item_3', 16 | }, 17 | { 18 | label: 'Item 4', 19 | value: 'item_4', 20 | }, 21 | { 22 | label: 'Item 5', 23 | value: 'item_5', 24 | }, 25 | ]; 26 | 27 | const Template = (arg) => { 28 | const [inputs, setInputs] = useState({}); 29 | const onChange = (e) => { 30 | const { name, value, type } = e.target; 31 | 32 | setInputs((prevState) => ({ 33 | ...prevState, 34 | [name]: type === 'checkbox' ? !prevState[name] : value, 35 | })); 36 | }; 37 | 38 | return ( 39 | <> 40 |

{arg.title}

41 | 48 | 49 | ); 50 | }; 51 | 52 | export const SelectComponent = Template.bind({}); 53 | SelectComponent.args = { title: 'Select' }; 54 | 55 | export const SelectWithLabel = Template.bind({}); 56 | SelectWithLabel.args = { title: 'Select with label', label: 'Label' }; 57 | 58 | export const SelectWithPlaceholder = Template.bind({}); 59 | SelectWithPlaceholder.args = { 60 | title: 'Select with placeholder', 61 | placeholder: 'Placeholder', 62 | }; 63 | 64 | export const SelectWithPlaceholderAndLabel = Template.bind({}); 65 | SelectWithPlaceholderAndLabel.args = { 66 | title: 'Select with placeholder and label', 67 | placeholder: 'Placeholder', 68 | label: 'Label', 69 | }; 70 | 71 | export const SelectWithPlaceholderAndLabelAndDisableShrink = Template.bind({}); 72 | SelectWithPlaceholderAndLabelAndDisableShrink.args = { 73 | title: 'Select with placeholder and label and disable shrink', 74 | placeholder: 'Placeholder', 75 | label: 'Label', 76 | disableShrink: true, 77 | }; 78 | 79 | export const SelectDisabled = Template.bind({}); 80 | SelectDisabled.args = { 81 | title: 'Disabled Select', 82 | placeholder: 'Placeholder', 83 | disabled: true, 84 | label: 'Label', 85 | }; 86 | 87 | export const SelectMulti = Template.bind({}); 88 | SelectMulti.args = { 89 | title: 'Select multiple items', 90 | label: 'Label', 91 | isMulti: true, 92 | }; 93 | 94 | export const SelectError = Template.bind({}); 95 | SelectError.args = { 96 | title: 'Select with error', 97 | label: 'Label', 98 | error: 'Error Message', 99 | }; 100 | 101 | const Component = { 102 | title: 'Form/Select', 103 | component: Index, 104 | }; 105 | 106 | export default Component; 107 | -------------------------------------------------------------------------------- /src/Select/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useId, 3 | forwardRef, 4 | useCallback, 5 | useMemo, 6 | useState, 7 | } from 'react'; 8 | import cx from 'clsx'; 9 | import { 10 | default as ReactSelect, 11 | components as SelectComponents, 12 | Props, 13 | GroupBase, 14 | SelectInstance, 15 | } from 'react-select'; 16 | import makeAnimated from 'react-select/animated'; 17 | 18 | import s from './Select.module.scss'; 19 | import { FieldError } from 'react-hook-form'; 20 | 21 | const animatedComponents = makeAnimated(); 22 | 23 | interface Option { 24 | value: string | number; 25 | label: string | React.ReactNode; 26 | } 27 | 28 | interface SelectProps extends Props> { 29 | name: string; 30 | error?: 31 | | boolean 32 | | string 33 | | { message?: string } 34 | | null 35 | | undefined 36 | | FieldError; 37 | label?: string | React.ReactNode; 38 | options?: Option[]; 39 | placeholder?: string; 40 | value?: Option | null; 41 | inputClassName?: string; 42 | labelClassName?: string; 43 | errorClassName?: string; 44 | disableShrink?: boolean; 45 | disabled?: boolean; 46 | classNames?: Record; 47 | components?: Record; 48 | onFocus?: (e: React.FocusEvent) => void; 49 | onBlur?: (e: React.FocusEvent) => void; 50 | [rest: string]: any; 51 | } 52 | 53 | const Select = forwardRef< 54 | SelectInstance>, 55 | SelectProps 56 | >( 57 | ( 58 | { 59 | name, 60 | onChange, 61 | error = null, 62 | label = null, 63 | options = [], 64 | placeholder = null, 65 | value = null, 66 | inputClassName = null, 67 | labelClassName = null, 68 | errorClassName = null, 69 | disableShrink = false, 70 | disabled = false, 71 | classNames = null, 72 | components = null, 73 | onFocus = () => ({}), 74 | onBlur = () => ({}), 75 | ...rest 76 | }, 77 | inputRef // inputRef is now typed as a ForwardedRef of StateManager (ReactSelect component) 78 | ) => { 79 | const [focused, setFocused] = useState(false); 80 | 81 | const handleChange = useCallback( 82 | (option: Option | null) => { 83 | onChange({ 84 | target: { 85 | name, 86 | value: option, 87 | }, 88 | }); 89 | }, 90 | [inputRef, name, onChange] 91 | ); 92 | 93 | const labelEl = useMemo( 94 | () => ( 95 | 109 | ), 110 | [ 111 | disabled, 112 | name, 113 | disableShrink, 114 | label, 115 | placeholder, 116 | focused, 117 | value, 118 | labelClassName, 119 | inputRef, 120 | ] 121 | ); 122 | 123 | const errorMessage = useMemo(() => { 124 | if (error && typeof error === 'string') { 125 | return error; 126 | } else if (error && typeof error === 'object' && error.message) { 127 | return error.message; 128 | } 129 | return null; 130 | }, [error]); 131 | 132 | return ( 133 |
134 |
135 | {label && disableShrink ? labelEl : null} 136 | 137 | s.innerInput, 145 | menu: () => s.menu, 146 | option: (state) => 147 | state.isFocused || state.isSelected 148 | ? s.optionFocused 149 | : s.option, 150 | singleValue: () => s.singleValue, 151 | placeholder: () => s.placeholder, 152 | indicatorSeparator: () => s.indicatorSeparator, 153 | indicatorsContainer: () => s.indicatorsContainer, 154 | clearIndicator: () => s.clearIndicator, 155 | multiValue: () => s.multiValue, 156 | valueContainer: () => 157 | label && !disableShrink ? s.valueContainer : null, 158 | multiValueRemove: () => s.multiValueRemove, 159 | ...classNames, 160 | control: (state) => 161 | cx(s.input, { 162 | [s.control]: !disableShrink && label, 163 | [s.focus]: state.isFocused, 164 | [s.inputError]: typeof error === 'boolean' && error, 165 | [s.disabled]: state.isDisabled, 166 | [inputClassName]: inputClassName, 167 | [classNames?.control?.(state)]: classNames?.control?.(state), 168 | }), 169 | }} 170 | placeholder={placeholder} 171 | name={name} 172 | isDisabled={disabled} 173 | onFocus={(e) => { 174 | onFocus(e); 175 | setFocused(true); 176 | }} 177 | onBlur={(e) => { 178 | onBlur(e); 179 | setFocused(false); 180 | }} 181 | components={{ ...animatedComponents, ...components }} 182 | {...rest} 183 | /> 184 | 185 | {label && !disableShrink ? labelEl : null} 186 |
187 | {errorMessage && ( 188 | 193 | {errorMessage} 194 | 195 | )} 196 |
197 | ); 198 | } 199 | ); 200 | 201 | Select.displayName = 'Select'; 202 | 203 | export default Select; 204 | 205 | export { SelectComponents, type SelectProps }; 206 | -------------------------------------------------------------------------------- /src/Slider/Slider.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | margin-bottom: var(--rfe-spacing); 6 | height: 100%; 7 | } 8 | 9 | .inputRoot { 10 | height: 100%; 11 | width: auto; 12 | } 13 | 14 | .label { 15 | margin-bottom: var(--rfe-label-spacing); 16 | font-size: var(--rfe-font-size); 17 | line-height: 20px; 18 | color: var(--rfe-color); 19 | pointer-events: all; 20 | cursor: pointer; 21 | } 22 | 23 | .slider { 24 | width: 100%; 25 | } 26 | 27 | .vertical { 28 | height: 200px; 29 | 30 | .tracks { 31 | width: 10px !important; 32 | background: var(--rfe-background) !important; 33 | border: 1px solid var(--rfe-border) !important; 34 | } 35 | 36 | .track { 37 | width: 10px !important; 38 | background: var(--rfe-background-selected) !important; 39 | border: 1px solid var(--rfe-border) !important; 40 | } 41 | 42 | .rail { 43 | height: 100% !important; 44 | width: 10px !important; 45 | background: var(--rfe-background) !important; 46 | border: 1px solid var(--rfe-border) !important; 47 | } 48 | 49 | .handle { 50 | background: var(--rfe-background) !important; 51 | border: 1px solid var(--rfe-border) !important; 52 | cursor: pointer !important; 53 | opacity: 1 !important; 54 | box-shadow: none !important; 55 | width: 20px !important; 56 | height: 20px !important; 57 | 58 | &:hover { 59 | border: 1px solid var(--rfe-border) !important; 60 | background: var(--rfe-background-selected) !important; 61 | box-shadow: var(--rfe-focus) !important; 62 | } 63 | } 64 | } 65 | 66 | .horizontal { 67 | .tracks { 68 | height: 10px !important; 69 | background: var(--rfe-background) !important; 70 | border: 1px solid var(--rfe-border) !important; 71 | } 72 | 73 | .track { 74 | height: 10px !important; 75 | background: var(--rfe-background-selected) !important; 76 | border: 1px solid var(--rfe-border) !important; 77 | } 78 | 79 | .rail { 80 | width: 100% !important; 81 | height: 10px !important; 82 | background: var(--rfe-background) !important; 83 | border: 1px solid var(--rfe-border) !important; 84 | } 85 | 86 | .handle { 87 | background: var(--rfe-background) !important; 88 | border: 1px solid var(--rfe-border) !important; 89 | cursor: pointer !important; 90 | opacity: 1 !important; 91 | box-shadow: none !important; 92 | width: 20px !important; 93 | height: 20px !important; 94 | 95 | &:hover { 96 | border: 1px solid var(--rfe-border) !important; 97 | background: var(--rfe-background-selected) !important; 98 | box-shadow: var(--rfe-focus) !important; 99 | } 100 | } 101 | } 102 | 103 | .disabled { 104 | background: none !important; 105 | cursor: not-allowed !important; 106 | 107 | .tracks { 108 | background: var(--rfe-background-disabled) !important; 109 | } 110 | 111 | .track { 112 | background: var(--rfe-background-disabled) !important; 113 | } 114 | 115 | .rail { 116 | background: var(--rfe-background-disabled) !important; 117 | } 118 | 119 | .handle { 120 | background: var(--rfe-background) !important; 121 | box-shadow: none !important; 122 | cursor: not-allowed !important; 123 | } 124 | } 125 | 126 | .error { 127 | .handle { 128 | border-color: var(--rfe-color-error) !important; 129 | } 130 | 131 | .rail { 132 | border-color: var(--rfe-color-error) !important; 133 | } 134 | 135 | .tracks { 136 | border-color: var(--rfe-color-error) !important; 137 | } 138 | 139 | .track { 140 | border-color: var(--rfe-color-error) !important; 141 | } 142 | } 143 | 144 | .slider:focus-within { 145 | .handle { 146 | box-shadow: var(--rfe-focus) !important; 147 | } 148 | } 149 | 150 | .errorLabel { 151 | color: var(--rfe-color-error); 152 | font-size: var(--rfe-error-font-size); 153 | margin-top: var(--rfe-label-spacing); 154 | pointer-events: none; 155 | } 156 | -------------------------------------------------------------------------------- /src/Slider/Slider.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Slider from './index'; 4 | 5 | const Template = (args) => { 6 | const [inputs, setInputs] = useState({}); 7 | const onChange = (e) => { 8 | const { name, value, type } = e.target; 9 | 10 | setInputs((prevState) => ({ 11 | ...prevState, 12 | [name]: type === 'checkbox' ? !prevState[name] : value, 13 | })); 14 | }; 15 | 16 | return ( 17 | <> 18 |

{args.title}

19 |
27 | 36 |
37 | 38 | ); 39 | }; 40 | 41 | export const SliderComponent = Template.bind({}); 42 | SliderComponent.args = { title: 'Slider' }; 43 | 44 | export const SliderComponentDisabled = Template.bind({}); 45 | SliderComponentDisabled.args = { 46 | title: 'Slider', 47 | disabled: true, 48 | }; 49 | 50 | export const SliderComponentMarks = Template.bind({}); 51 | SliderComponentMarks.args = { 52 | title: 'Slider', 53 | disabled: false, 54 | marks: { 55 | 0: 0°C, 56 | 26: '26°C', 57 | 37: '37°C', 58 | 50: '50°C', 59 | 100: { 60 | style: { 61 | color: 'red', 62 | }, 63 | label: 100°C, 64 | }, 65 | }, 66 | }; 67 | 68 | export const SliderComponentMarksVertical = Template.bind({}); 69 | SliderComponentMarksVertical.args = { 70 | title: 'Slider', 71 | vertical: true, 72 | marks: { 73 | 0: 0°C, 74 | 26: '26°C', 75 | 37: '37°C', 76 | 50: '50°C', 77 | 100: { 78 | style: { 79 | color: 'red', 80 | }, 81 | label: 100°C, 82 | }, 83 | }, 84 | }; 85 | 86 | export const SliderRangeComponent = Template.bind({}); 87 | SliderRangeComponent.args = { 88 | title: 'Slider Range', 89 | range: true, 90 | defaultValue: [20, 50], 91 | }; 92 | 93 | export const SliderVerticalComponent = Template.bind({}); 94 | SliderVerticalComponent.args = { 95 | title: 'Slider Vertical', 96 | defaultValue: 50, 97 | vertical: true, 98 | }; 99 | 100 | export const SliderComponentError = Template.bind({}); 101 | SliderComponentError.args = { 102 | title: 'Slider with error', 103 | error: 'Error Message', 104 | }; 105 | 106 | const Component = { 107 | title: 'Form/Slider', 108 | component: SliderComponent, 109 | }; 110 | 111 | export default Component; 112 | -------------------------------------------------------------------------------- /src/Slider/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useMemo } from 'react'; 2 | import cx from 'clsx'; 3 | import Slider from 'rc-slider'; 4 | import s from './Slider.module.scss'; 5 | import { FieldError } from 'react-hook-form'; 6 | 7 | interface SliderProps { 8 | name: string; 9 | onChange: (event: { 10 | target: { name: string; value: number | number[] }; 11 | }) => void; 12 | label?: string | React.ReactNode; 13 | value?: number | number[] | null; 14 | range?: boolean; 15 | vertical?: boolean; 16 | defaultValue?: number[]; 17 | tracksClassName?: string; 18 | trackClassName?: string; 19 | railClassName?: string; 20 | handleClassName?: string; 21 | labelClassName?: string; 22 | errorClassName?: string; 23 | disabled?: boolean; 24 | error?: 25 | | boolean 26 | | string 27 | | { message?: string } 28 | | null 29 | | undefined 30 | | FieldError; 31 | [rest: string]: any; 32 | } 33 | 34 | const SliderComponent = forwardRef( 35 | ( 36 | { 37 | name, 38 | onChange, 39 | label = null, 40 | value = null, 41 | range = false, 42 | vertical = false, 43 | defaultValue = [0, 0], 44 | tracksClassName = null, 45 | trackClassName = null, 46 | railClassName = null, 47 | handleClassName = null, 48 | labelClassName = null, 49 | errorClassName = null, 50 | disabled = false, 51 | error = null, 52 | ...rest 53 | }, 54 | inputRef 55 | ) => { 56 | const errorMessage = useMemo(() => { 57 | if (error && typeof error === 'string') { 58 | return error; 59 | } else if (error && typeof error === 'object' && error.message) { 60 | return error.message; 61 | } 62 | return null; 63 | }, [error]); 64 | 65 | const handleOnChange = (value: number | number[]) => { 66 | onChange({ 67 | target: { 68 | name, 69 | value: value, 70 | }, 71 | }); 72 | }; 73 | 74 | return ( 75 |
76 | {label && ( 77 |
82 | {label} 83 |
84 | )} 85 |
86 | 131 |
132 | {errorMessage && ( 133 |
138 | {errorMessage} 139 |
140 | )} 141 |
142 | ); 143 | } 144 | ); 145 | 146 | SliderComponent.displayName = 'Slider'; 147 | 148 | export default SliderComponent; 149 | 150 | export { type SliderProps }; 151 | -------------------------------------------------------------------------------- /src/Switch/Switch.module.scss: -------------------------------------------------------------------------------- 1 | .switch { 2 | display: inline-flex; 3 | flex-direction: column; 4 | position: relative; 5 | margin-bottom: var(--rfe-spacing); 6 | } 7 | 8 | .inputRoot { 9 | display: flex; 10 | align-items: center; 11 | } 12 | 13 | .label { 14 | margin-top: 1px; 15 | font-size: var(--rfe-font-size); 16 | line-height: 20px; 17 | color: var(--rfe-color); 18 | pointer-events: all; 19 | cursor: pointer; 20 | } 21 | 22 | .errorLabel { 23 | color: var(--rfe-color-error); 24 | font-size: var(--rfe-error-font-size); 25 | margin-top: var(--rfe-label-spacing); 26 | pointer-events: none; 27 | } 28 | 29 | .switchCheckbox { 30 | position: fixed; 31 | top: 0; 32 | left: 0; 33 | z-index: 0; 34 | opacity: 0; 35 | width: 0; 36 | height: 0; 37 | } 38 | 39 | .switchCheckbox:focus-visible ~ .switchLabel { 40 | box-shadow: var(--rfe-focus); 41 | } 42 | 43 | .switchLabel { 44 | display: block; 45 | overflow: hidden; 46 | cursor: pointer; 47 | border-radius: 20px; 48 | border: var(--rfe-border) 1px solid; 49 | background-color: var(--rfe-background); 50 | height: 28px; 51 | width: 56px; 52 | min-width: 56px; 53 | position: relative; 54 | margin: 0 16px 0 0; 55 | transition: all 250ms; 56 | 57 | &:hover { 58 | box-shadow: var(--rfe-focus); 59 | } 60 | } 61 | 62 | .switchSwitch { 63 | display: block; 64 | width: 20px; 65 | height: 20px; 66 | margin: 1px; 67 | background: var(--rfe-background); 68 | position: absolute; 69 | top: 2px; 70 | bottom: 0; 71 | right: 29px; 72 | border-radius: 100px; 73 | border: var(--rfe-border) 1px solid; 74 | } 75 | 76 | .switchInnerChecked { 77 | margin-left: 0; 78 | background: var(--rfe-background-selected); 79 | } 80 | 81 | .noLabel { 82 | margin: 0; 83 | } 84 | 85 | .switchSwitchChecked { 86 | right: 2px; 87 | } 88 | 89 | .inputError { 90 | border-color: var(--rfe-color-error) !important; 91 | } 92 | 93 | .disabled { 94 | box-shadow: none !important; 95 | cursor: not-allowed !important; 96 | background: var(--rfe-background-disabled); 97 | 98 | & span { 99 | background: var(--rfe-background); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Switch/Switch.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Switch from './index'; 4 | 5 | const Template = (args) => { 6 | const [inputs, setInputs] = useState({}); 7 | const onChange = (e) => { 8 | const { name, value, type } = e.target; 9 | 10 | setInputs((prevState) => ({ 11 | ...prevState, 12 | [name]: type === 'checkbox' ? !prevState[name] : value, 13 | })); 14 | }; 15 | 16 | return ( 17 | <> 18 |

{args.title}

19 |
26 | 33 |
34 | 35 | ); 36 | }; 37 | 38 | export const SwitchComponent = Template.bind({}); 39 | SwitchComponent.args = { title: 'Switch' }; 40 | 41 | export const SwitchComponentChecked = Template.bind({}); 42 | SwitchComponentChecked.args = { title: 'Switch', checked: true }; 43 | 44 | export const SwitchComponentDisabled = Template.bind({}); 45 | SwitchComponentDisabled.args = { title: 'Switch', disabled: true }; 46 | 47 | export const SwitchComponentError = Template.bind({}); 48 | SwitchComponentError.args = { 49 | title: 'Switch with error', 50 | error: 'Error Message', 51 | }; 52 | 53 | const Component = { 54 | title: 'Form/Switch', 55 | component: SwitchComponent, 56 | }; 57 | 58 | export default Component; 59 | -------------------------------------------------------------------------------- /src/Switch/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useMemo } from 'react'; 2 | import cx from 'clsx'; 3 | import s from './Switch.module.scss'; 4 | import { FieldError } from 'react-hook-form'; 5 | 6 | interface SwitchProps { 7 | name: string; 8 | onChange: (event: React.ChangeEvent) => void; 9 | label?: string | React.ReactNode; 10 | checked?: boolean; 11 | inputClassName?: string; 12 | labelClassName?: string; 13 | errorClassName?: string; 14 | disabled?: boolean; 15 | error?: 16 | | boolean 17 | | string 18 | | { message?: string } 19 | | null 20 | | undefined 21 | | FieldError; 22 | } 23 | 24 | const Switch = forwardRef( 25 | ( 26 | { 27 | name, 28 | onChange, 29 | label = null, 30 | checked = false, 31 | inputClassName = null, 32 | labelClassName = null, 33 | errorClassName = null, 34 | disabled = false, 35 | error = null, 36 | }, 37 | inputRef 38 | ) => { 39 | const errorMessage = useMemo(() => { 40 | if (error && typeof error === 'string') { 41 | return error; 42 | } else if (error && typeof error === 'object' && error.message) { 43 | return error.message; 44 | } 45 | return null; 46 | }, [error]); 47 | 48 | return ( 49 | 94 | ); 95 | } 96 | ); 97 | 98 | Switch.displayName = 'Switch'; 99 | 100 | export default Switch; 101 | 102 | export { type SwitchProps }; 103 | -------------------------------------------------------------------------------- /src/TextArea/TextArea.module.scss: -------------------------------------------------------------------------------- 1 | .inputContainer { 2 | position: relative; 3 | margin-bottom: var(--rfe-spacing); 4 | box-sizing: border-box; 5 | } 6 | 7 | .label { 8 | font-size: var(--rfe-font-size); 9 | line-height: 1; 10 | color: var(--rfe-color); 11 | margin-bottom: 0; 12 | position: absolute; 13 | top: -2px; 14 | left: 16px; 15 | cursor: text; 16 | transform: translateY(calc((var(--rfe-input-height) - 14px) / 2)); 17 | transition: 18 | transform 250ms, 19 | font-size 250ms; 20 | width: calc(100% - 16px); 21 | 22 | &::before { 23 | content: ''; 24 | position: absolute; 25 | top: -6px; 26 | left: 0; 27 | width: calc(100% - 14px); 28 | height: 24px; 29 | background-color: var(--rfe-background); 30 | transition: all 250ms; 31 | z-index: -1; 32 | } 33 | } 34 | 35 | .inputContainer.focusedContainer .label { 36 | font-size: var(--rfe-focused-font-size); 37 | transform: translateY(9px); 38 | } 39 | 40 | .inputContainer .innerContainer { 41 | display: flex; 42 | flex-direction: column; 43 | position: relative; 44 | } 45 | 46 | .inputContainer .innerWrapper { 47 | display: flex; 48 | flex-direction: column; 49 | border-radius: var(--rfe-border-radius); 50 | } 51 | 52 | .input { 53 | background-color: var(--rfe-background); 54 | resize: none; 55 | color: var(--rfe-color); 56 | padding: 7px 16px; 57 | width: 100%; 58 | line-height: 24px; 59 | min-height: calc(var(--rfe-input-height) * 2); 60 | border-radius: var(--rfe-border-radius); 61 | border: 1px solid var(--rfe-border); 62 | transition: all 250ms; 63 | font-family: var(--rfe-font-family); 64 | } 65 | 66 | .inputContainer .innerWrapper .input.withLabel { 67 | padding-top: 25px; 68 | padding-bottom: 0; 69 | } 70 | 71 | .inputContainer .innerWrapper .input:focus { 72 | box-shadow: var(--rfe-focus); 73 | } 74 | 75 | .errorLabel { 76 | color: var(--rfe-color-error); 77 | font-size: var(--rfe-error-font-size); 78 | margin-top: var(--rfe-label-spacing); 79 | pointer-events: none; 80 | } 81 | 82 | .disabled { 83 | background-color: var(--rfe-background-disabled) !important; 84 | color: var(--rfe-color) !important; 85 | cursor: not-allowed !important; 86 | opacity: 1 !important; 87 | } 88 | 89 | .inputError { 90 | border-color: var(--rfe-color-error) !important; 91 | } 92 | 93 | .disabledLabel { 94 | pointer-events: none; 95 | cursor: not-allowed; 96 | 97 | &::before { 98 | background-color: var(--rfe-background-disabled) !important; 99 | } 100 | } 101 | 102 | .disableShrink { 103 | display: block; 104 | position: static !important; 105 | font-size: var(--rfe-font-size) !important; 106 | color: var(--rfe-color); 107 | transform: none; 108 | margin-bottom: var(--rfe-label-spacing); 109 | 110 | &::before { 111 | background-color: var(--rfe-transparent) !important; 112 | } 113 | } 114 | 115 | .disableShrinkInput { 116 | padding-top: 10px !important; 117 | padding-bottom: 0 !important; 118 | } 119 | 120 | .input::placeholder { 121 | font-size: var(--rfe-placeholder-font-size); 122 | color: var(--rfe-color-placeholder); 123 | } 124 | 125 | .labelPlaceholder { 126 | font-size: var(--rfe-focused-font-size); 127 | transform: translateY(9px); 128 | } 129 | -------------------------------------------------------------------------------- /src/TextArea/TextArea.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Index from './index'; 4 | 5 | const Template = (args) => { 6 | const [inputs, setInputs] = useState({ 7 | entered: 'Input Entered', 8 | }); 9 | const onChange = (e) => { 10 | const { name, value } = e.target; 11 | 12 | setInputs((prevState) => ({ 13 | ...prevState, 14 | [name]: value, 15 | })); 16 | }; 17 | 18 | return ( 19 | <> 20 |

{args.title}

21 | 22 | 23 | ); 24 | }; 25 | 26 | export const TextAreaComponent = Template.bind({}); 27 | TextAreaComponent.args = { title: 'TextArea' }; 28 | 29 | export const TextAreaComponentLabel = Template.bind({}); 30 | TextAreaComponentLabel.args = { title: 'TextArea', label: 'TextArea Label' }; 31 | 32 | export const TextAreaComponentPlaceholder = Template.bind({}); 33 | TextAreaComponentPlaceholder.args = { 34 | title: 'TextArea', 35 | placeholder: 'TextArea Placeholder', 36 | }; 37 | 38 | export const TextAreaComponentPlaceholderAndLabel = Template.bind({}); 39 | TextAreaComponentPlaceholderAndLabel.args = { 40 | title: 'TextArea', 41 | placeholder: 'TextArea Placeholder', 42 | label: 'TextArea Label', 43 | disabled: true, 44 | }; 45 | 46 | export const TextAreaComponentResizable = Template.bind({}); 47 | TextAreaComponentResizable.args = { 48 | title: 'TextArea', 49 | placeholder: 'TextArea Placeholder', 50 | label: 'TextArea Label', 51 | autoGrow: true, 52 | }; 53 | 54 | export const TextAreaComponentError = Template.bind({}); 55 | TextAreaComponentError.args = { 56 | title: 'TextArea', 57 | label: 'TextArea Label', 58 | error: 'Error Message', 59 | }; 60 | 61 | export const TextAreaComponentDisableShrink = Template.bind({}); 62 | TextAreaComponentDisableShrink.args = { 63 | title: 'TextArea', 64 | label: 'Label', 65 | placeholder: 'Placeholder', 66 | disableShrink: true, 67 | }; 68 | 69 | const Component = { 70 | title: 'Form/TextArea', 71 | component: Index, 72 | }; 73 | 74 | export default Component; 75 | -------------------------------------------------------------------------------- /src/TextArea/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | forwardRef, 3 | useCallback, 4 | useEffect, 5 | useMemo, 6 | useRef, 7 | useState, 8 | ChangeEvent, 9 | FocusEvent, 10 | RefObject, 11 | } from 'react'; 12 | import cx from 'clsx'; 13 | import s from './TextArea.module.scss'; 14 | import { FieldError } from 'react-hook-form'; 15 | 16 | type TextAreaProps = { 17 | name: string; 18 | onChange: (e: { target: { name: string; value: string } }) => void; 19 | value?: string; 20 | label?: string | null; 21 | placeholder?: string | null; 22 | disabled?: boolean; 23 | autoGrow?: boolean; 24 | disableShrink?: boolean; 25 | inputClassName?: string; 26 | labelClassName?: string; 27 | errorClassName?: string; 28 | onFocus?: (e: FocusEvent) => void; 29 | onBlur?: (e: FocusEvent) => void; 30 | error?: 31 | | boolean 32 | | string 33 | | { message?: string } 34 | | null 35 | | undefined 36 | | FieldError; 37 | } & React.TextareaHTMLAttributes; 38 | 39 | const TextArea = forwardRef( 40 | ( 41 | { 42 | name, 43 | onChange, 44 | error = null, 45 | value = '', 46 | placeholder = null, 47 | label = null, 48 | disabled = false, 49 | inputClassName = null, 50 | labelClassName = null, 51 | errorClassName = null, 52 | onFocus = () => ({}), 53 | onBlur = () => ({}), 54 | autoGrow = false, 55 | disableShrink = false, 56 | ...rest 57 | }, 58 | ref: RefObject 59 | ) => { 60 | const inputRef = useRef(ref?.current || null); 61 | const [focused, setFocused] = useState(false); 62 | 63 | const resizeTextArea = () => { 64 | if (autoGrow && inputRef.current) { 65 | inputRef.current.style.height = 'auto'; 66 | inputRef.current.style.height = inputRef.current.scrollHeight + 'px'; 67 | } 68 | }; 69 | 70 | useEffect(resizeTextArea, [autoGrow, value]); 71 | 72 | const errorMessage = useMemo(() => { 73 | let message: string | null = null; 74 | if (error && typeof error === 'string') { 75 | message = error; 76 | } else if (error && typeof error === 'object' && error?.message) { 77 | message = error.message; 78 | } 79 | return message; 80 | }, [error]); 81 | 82 | const handleChange = useCallback( 83 | (e: ChangeEvent) => { 84 | onChange({ 85 | target: { 86 | name: e.target.name, 87 | value: e.target.value, 88 | }, 89 | }); 90 | }, 91 | [onChange] 92 | ); 93 | 94 | const setFocusState = useCallback(() => { 95 | setFocused(true); 96 | inputRef.current?.focus(); 97 | }, [inputRef]); 98 | 99 | const textArea = useMemo(() => { 100 | return ( 101 |