├── .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 |
15 |
16 |
17 |
18 |
19 |
20 | ---
21 |
22 | 
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 |
139 |
140 |
141 | yunusozcan
142 |
143 |
144 |
145 |
146 |
147 | emreonursoy
148 |
149 |
150 |
151 |
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 |
113 | {label}
114 |
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 |
72 | {label}
73 |
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 |
51 |
52 |
62 |
70 |
71 |
72 |
77 | {label}
78 |
79 |
80 | {errorMessage ? (
81 |
86 | {errorMessage}
87 |
88 | ) : null}
89 |
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 |
87 | {label && (
88 |
93 | {label}
94 |
95 | )}
96 |
102 | {options.map((option, index) => (
103 |
111 | handleChange(
112 | value ? value.includes(option.value) : false,
113 | option.value
114 | )
115 | }
116 | >
117 | handleChange(e.target.checked, option.value)}
131 | />
132 |
133 | ))}
134 |
135 | {errorMessage && (
136 |
141 | {errorMessage}
142 |
143 | )}
144 |
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 |
81 | {label && (
82 |
87 | {label}
88 |
89 | )}
90 | {options.map((option, index) => {
91 | return (
92 | handleChange(e.target.checked, option.value)}
104 | />
105 | );
106 | })}
107 | {errorMessage && (
108 |
113 | {errorMessage}
114 |
115 | )}
116 |
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 |
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 | {
125 | try {
126 | const inputs = document.querySelectorAll(
127 | `[name="${name}"]`
128 | );
129 |
130 | if (!inputs.length) {
131 | return;
132 | }
133 |
134 | let input = inputs[0];
135 |
136 | if (input?.type === 'hidden') {
137 | input = input?.parentNode?.querySelector(
138 | 'input'
139 | ) as HTMLInputElement;
140 | }
141 |
142 | input?.focus();
143 | } catch (error) {
144 | throw error;
145 | }
146 | }}
147 | >
148 | {label}
149 |
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 |
44 |
49 |
53 |
54 |
55 |
56 |
57 |
61 |
62 |
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 |
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 |
98 | {label}
99 |
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 |
191 | {resendLabel}
192 |
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 |
55 |
56 |
68 |
76 |
77 |
78 |
83 | {label}
84 |
85 |
86 | {errorMessage && (
87 |
92 | {errorMessage}
93 |
94 | )}
95 |
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 |
75 |
80 | {label}
81 |
82 |
88 | {options.map((option, index) => (
89 |
handleClick(option.value)}
97 | >
98 |
112 |
113 | ))}
114 |
115 | {errorMessage && (
116 |
121 | {errorMessage}
122 |
123 | )}
124 |
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 |
58 |
63 | {label}
64 |
65 | {options.map((option, index) => (
66 |
79 | ))}
80 | {errorMessage && (
81 |
86 | {errorMessage}
87 |
88 | )}
89 |
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 |
107 | {label}
108 |
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 |
50 |
51 |
61 |
70 |
75 |
76 |
81 | {label}
82 |
83 |
84 | {errorMessage ? (
85 |
90 | {errorMessage}
91 |
92 | ) : null}
93 |
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 |