intelicoreact 0.0.78 → 0.0.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ .input {
2
+ position: relative;
3
+ word-break: break-all;
4
+ border: none;
5
+ background: none;
6
+ padding: 0 10px;
7
+ width: 100%;
8
+ &::-webkit-outer-spin-button,
9
+ &::-webkit-inner-spin-button {
10
+ -webkit-appearance: none;
11
+ }
12
+
13
+ &__wrap {
14
+ display: flex;
15
+ align-items: center;
16
+ border: 1px solid #e2e5ec;
17
+ box-sizing: border-box;
18
+ background-color: #fff;
19
+ height: 28px;
20
+ border-radius: 4px;
21
+
22
+ &_focus {
23
+ border-color: #6b81dd;
24
+ filter: drop-shadow(0px 0px 4px rgba(93, 120, 255, 0.5));
25
+ }
26
+
27
+ &_disabled {
28
+ background: #f7f8fa;
29
+ opacity: 0.5;
30
+ border-color: #a6acb1;
31
+ pointer-events: none;
32
+ }
33
+
34
+ &_error {
35
+ border-color: #f06d8d;
36
+ }
37
+ }
38
+
39
+ &_disabled {
40
+ background: #f7f8fa;
41
+ opacity: 0.5;
42
+ border-color: #a6acb1;
43
+ pointer-events: none;
44
+ }
45
+
46
+ &_error {
47
+ border-color: #f06d8d;
48
+ }
49
+
50
+ svg {
51
+ margin-right: 4px;
52
+ }
53
+
54
+ &__input {
55
+ width: 100%;
56
+ height: 100%;
57
+ box-sizing: border-box;
58
+ font-size: 13px;
59
+ font-weight: 400;
60
+ color: #1e1e2d;
61
+ border: none;
62
+ padding: 0 5px;
63
+ border-radius: 4px;
64
+ }
65
+
66
+ &__close {
67
+ position: relative;
68
+ opacity: 0.6;
69
+ width: 14px;
70
+ height: 14px;
71
+ background: none;
72
+ cursor: pointer;
73
+ margin-right: 4px;
74
+ visibility: hidden;
75
+ pointer-events: none;
76
+ &:hover {
77
+ opacity: 1;
78
+ }
79
+
80
+ &:before,
81
+ &:after {
82
+ content: '';
83
+ position: absolute;
84
+ top: 0;
85
+ left: 0;
86
+ right: 0;
87
+ margin: auto;
88
+ height: 14px;
89
+ width: 2px;
90
+ background-color: #9aa0b9;
91
+ }
92
+
93
+ &:before {
94
+ transform: rotate(45deg);
95
+ }
96
+
97
+ &:after {
98
+ transform: rotate(-45deg);
99
+ }
100
+ }
101
+
102
+ &__nums {
103
+ display: flex;
104
+ align-items: center;
105
+ height: 100%;
106
+ }
107
+
108
+ &__num-btn {
109
+ cursor: pointer;
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ height: 100%;
114
+ width: 24px;
115
+ border-left: 1px solid #e2e5ec;
116
+ background: none;
117
+ font-size: 20px;
118
+ user-select: none;
119
+ &.disabled {
120
+ opacity: 0.3;
121
+ }
122
+ svg {
123
+ margin-right: 0;
124
+ }
125
+ }
126
+
127
+ &-label {
128
+ margin-bottom: 5px;
129
+ }
130
+ }
131
+
132
+ .input__wrap:hover .input__close {
133
+ visibility: visible;
134
+ pointer-events: all;
135
+ }
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ var _typeof = require("@babel/runtime/helpers/typeof");
6
+
7
+ Object.defineProperty(exports, "__esModule", {
8
+ value: true
9
+ });
10
+ exports.NumericInputTemplate = exports.default = void 0;
11
+
12
+ var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
13
+
14
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
15
+
16
+ var _react = _interopRequireWildcard(require("react"));
17
+
18
+ var _reactFeather = require("react-feather");
19
+
20
+ var _NumericInput = _interopRequireDefault(require("./NumericInput"));
21
+
22
+ function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
23
+
24
+ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
25
+
26
+ global.lng = 'en';
27
+ var _default = {
28
+ title: 'Form Elements/NumericInput',
29
+ component: _NumericInput.default,
30
+ argTypes: {
31
+ disabled: {
32
+ description: 'boolean'
33
+ },
34
+ isFocusDefault: {
35
+ description: 'boolean - if true, input will be focused on mount'
36
+ },
37
+ isInitialFocus: {
38
+ description: 'boolean - if true, the input will be focused on mount'
39
+ },
40
+ error: {
41
+ description: 'text - coloring input if is errored'
42
+ },
43
+ isPriceInput: {
44
+ description: 'boolean - if true, the input will be styled as PriceInput'
45
+ },
46
+ withDelete: {
47
+ description: 'boolean - add clear-cross by hover'
48
+ },
49
+ numStep: {
50
+ description: 'number/text - plus/minus buttons factor (default: 1)'
51
+ },
52
+ min: {
53
+ description: 'number/text - minimal number for numeric input'
54
+ },
55
+ max: {
56
+ description: 'number/text - maximal number for numeric input'
57
+ },
58
+ placeholder: {
59
+ description: 'text'
60
+ },
61
+ icon: {
62
+ description: 'JSX'
63
+ },
64
+ value: {
65
+ description: '(* - required prop)'
66
+ },
67
+ className: {
68
+ description: 'string'
69
+ },
70
+ mask: {
71
+ description: 'string: force input to masked https://www.npmjs.com/package/react-input-mask'
72
+ },
73
+ symbolsLimit: {
74
+ description: 'Set limit of symbols in input, overhead will be ignored'
75
+ },
76
+ onBlur: {
77
+ description: 'custom callback on blur'
78
+ },
79
+ onFocus: {
80
+ description: 'custom callback on focus'
81
+ },
82
+ onKeyUp: {
83
+ description: 'custom callback on keyup, returns event keyCode'
84
+ }
85
+ }
86
+ };
87
+ exports.default = _default;
88
+
89
+ var Template = function Template(args) {
90
+ var _useState = (0, _react.useState)('15000'),
91
+ _useState2 = (0, _slicedToArray2.default)(_useState, 2),
92
+ value = _useState2[0],
93
+ setValue = _useState2[1];
94
+
95
+ return /*#__PURE__*/_react.default.createElement(_NumericInput.default, (0, _extends2.default)({}, args, {
96
+ value: value,
97
+ onChange: setValue
98
+ }));
99
+ };
100
+
101
+ var NumericInputTemplate = Template.bind({});
102
+ exports.NumericInputTemplate = NumericInputTemplate;
103
+ NumericInputTemplate.args = {
104
+ disabled: false,
105
+ isFocusDefault: false,
106
+ error: '',
107
+ isPriceInput: false,
108
+ mask: '',
109
+ withDelete: true,
110
+ isNumeric: false,
111
+ numStep: 100,
112
+ min: '0',
113
+ max: '15000',
114
+ symbolsLimit: 5,
115
+ placeholder: 'Placeholder',
116
+ icon: /*#__PURE__*/_react.default.createElement(_reactFeather.User, null)
117
+ };
@@ -115,7 +115,9 @@ var RangeCalendar = function RangeCalendar(props) {
115
115
  onMouseLeave: function onMouseLeave() {
116
116
  return onHover(null);
117
117
  }
118
- }, day && day.date.getDate());
118
+ }, /*#__PURE__*/_react.default.createElement("span", {
119
+ className: "calendar__day-num"
120
+ }, day && day.date.getDate()));
119
121
  };
120
122
 
121
123
  var handlePrev = function handlePrev() {
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.formatInput = void 0;
9
+
10
+ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
11
+
12
+ var formatInput = {
13
+ priceInput: {
14
+ addCommas: function addCommas(value) {
15
+ value = value.toString();
16
+ var isFraction = value.includes('.');
17
+ var valueBeforeDot = isFraction ? value.slice(0, value.indexOf('.')) : value;
18
+ var intPart = valueBeforeDot.split('').reverse().reduce(function (acc, item, idx) {
19
+ return idx % 3 === 0 && idx !== 0 ? [].concat((0, _toConsumableArray2.default)(acc), [',', item]) : [].concat((0, _toConsumableArray2.default)(acc), [item]);
20
+ }, []).reverse().join('');
21
+ return isFraction ? intPart + value.slice(value.indexOf('.')) : intPart;
22
+ },
23
+ removeComma: function removeComma(value) {
24
+ return parseInt(value.toString().replace(/\,/g, ''));
25
+ }
26
+ },
27
+ onlyNumbers: function onlyNumbers(value) {
28
+ var isDot = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
29
+ var val = value.slice(0, 1) !== '0' && value.slice(0, 1) !== '.' ? value : value.slice(1);
30
+ if (isDot) return twoDigitAfterDot(val.replace(/[^0-9.]/g, ''));else return +val.toString().replace(/\D/g, '');
31
+ },
32
+ onlyString: function onlyString(value) {
33
+ return value.toString().replace(/[^a-z]/gi, '');
34
+ }
35
+ }; //обрезает числа после точки до 2х
36
+ // 342.23423432 -> 342.23
37
+
38
+ exports.formatInput = formatInput;
39
+
40
+ var twoDigitAfterDot = function twoDigitAfterDot(value) {
41
+ if (value.includes('.')) {
42
+ var valueAfterDot = value.slice(0, value.indexOf('.') + 3);
43
+ var rest = value.slice(value.indexOf('.') + 1, value.indexOf('.') + 3);
44
+ return allButTheFirstDotCutter(valueAfterDot);
45
+ } else {
46
+ return value;
47
+ }
48
+ }; //обрезает все точки кроме первой.
49
+ //для фомата "2 цифры после точки"
50
+ // нельзя = 123...
51
+ // можно 123.99
52
+
53
+
54
+ function allButTheFirstDotCutter(str) {
55
+ return str.replace(/^([^.]*\.)(.*)$/, function (a, b, c) {
56
+ return b + c.replace(/\./g, '');
57
+ });
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intelicoreact",
3
- "version": "0.0.78",
3
+ "version": "0.0.83",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -1,34 +1,35 @@
1
1
  import React, { useState, useEffect, useRef } from 'react';
2
2
  import cn from 'classnames';
3
- import { Minus, Plus } from 'react-feather';
4
3
  import InputMask from 'react-input-mask';
5
4
  import { KEYBOARD_SERVICE_KEYS } from '../../../Constants/index.constants';
5
+ import { formatInput } from '../../../Functions/inputExecutor';
6
6
 
7
7
  import './Input.scss';
8
8
 
9
9
  const Input = ({
10
10
  onChange,
11
+ onBlur,
12
+ onFocus,
13
+ onKeyUp,
14
+ isNotBlinkErrors,
15
+ isPriceInput,
16
+ isOnlyNumber,
17
+ isOnlyString,
18
+ isTwoDigitAfterDot,
11
19
  disabled,
12
20
  withDelete,
13
- isNumeric,
14
- numStep = 1,
15
- min = 0,
16
- max,
17
21
  value,
18
22
  placeholder,
19
23
  className,
20
24
  type = 'text',
21
- onBlur,
22
- onFocus,
23
- onKeyUp,
24
25
  mask,
25
26
  maskChar,
26
27
  formatChars,
27
28
  error,
28
29
  icon,
29
30
  symbolsLimit,
30
- isNotBlinkErrors,
31
31
  blinkTime,
32
+ isFocusDefault = false
32
33
  }) => {
33
34
  const DEFAULT_BLINK_TIME = 100;
34
35
  // STATES
@@ -38,42 +39,63 @@ const Input = ({
38
39
  const previousValueRef = useRef(value);
39
40
  const [isAttemptToChange, setIsAttemptToChange] = useState(false);
40
41
  const [isToHighlightError, setIsToHighlightError] = useState(false);
42
+
43
+ const { onlyNumbers, onlyString } = formatInput;
44
+ const { addCommas, removeComma } = formatInput.priceInput;
41
45
  // HANDLES
42
46
  const handle = {
43
- change: e => {
47
+ change: (e) => {
44
48
  let inputValue = e.target ? e.target.value : e;
45
- if (isNumeric || (type === 'number' && inputValue !== '')) {
46
- inputValue = parseFloat(inputValue) || '';
47
- if (min && +min > inputValue) {
48
- inputValue = min;
49
- } else if (max && +max < inputValue) inputValue = max;
50
- } else if (symbolsLimit && inputValue.length > +symbolsLimit) inputValue = inputValue.substring(0, +symbolsLimit);
49
+ if (symbolsLimit && inputValue.length > +symbolsLimit) {
50
+ inputValue = inputValue.substring(0, +symbolsLimit);
51
+ }
52
+
53
+ if (isOnlyNumber && !isTwoDigitAfterDot)
54
+ inputValue = onlyNumbers(inputValue);
55
+ if (isTwoDigitAfterDot) inputValue = onlyNumbers(inputValue, true);
56
+ if (isOnlyString) inputValue = onlyString(inputValue);
57
+
51
58
  onChange(inputValue.toString());
52
59
  },
53
60
  toggleEdit: () => {
54
61
  setEditing(!isEditing);
55
62
  onChange('');
56
63
  },
57
- focus: e => {
64
+ focus: (e) => {
58
65
  setIsFocused(true);
66
+ if (isPriceInput && isOnlyNumber) onChange(removeComma(value));
67
+
59
68
  if (onFocus) onFocus(e);
60
69
  },
61
- blur: e => {
70
+ blur: (e) => {
62
71
  setIsFocused(false);
63
72
  setEditing(false);
73
+ if (isTwoDigitAfterDot) {
74
+ onChange(cutOffsingleDot(value));
75
+ }
76
+ if (isPriceInput && isOnlyNumber) {
77
+ onChange(addCommas(value));
78
+ }
64
79
  if (onBlur) onBlur(e);
65
80
  },
66
- keyUp: e => {
81
+ keyUp: (e) => {
67
82
  if (!isNotBlinkErrors) {
68
83
  const changedValue = '' + (value ?? '');
69
84
  const previousValue = '' + (previousValueRef.current ?? '');
70
85
  const currentSet = (() => {
71
- if (previousValue.length < changedValue.length) return value.slice(previousValue.length - changedValue.length);
86
+ if (previousValue.length < changedValue.length)
87
+ return value.slice(previousValue.length - changedValue.length);
72
88
  else return changedValue.includes(e.key) ? e.key : '';
73
89
  })();
74
90
 
75
- if(!KEYBOARD_SERVICE_KEYS.includes(e.key) && changedValue === previousValue) setIsAttemptToChange(true);
76
- if (KEYBOARD_SERVICE_KEYS.includes(e.key) || !currentSet) previousValueRef.current = value;
91
+ if (
92
+ !KEYBOARD_SERVICE_KEYS.includes(e.key) &&
93
+ changedValue === previousValue
94
+ )
95
+ setIsAttemptToChange(true);
96
+
97
+ if (KEYBOARD_SERVICE_KEYS.includes(e.key) || !currentSet)
98
+ previousValueRef.current = value;
77
99
  else previousValueRef.current = previousValue + currentSet[0];
78
100
  }
79
101
 
@@ -81,6 +103,10 @@ const Input = ({
81
103
  }
82
104
  };
83
105
 
106
+ function cutOffsingleDot(value) {
107
+ return value.toString().slice(-1) === '.' ? value.slice(0, -1) : value;
108
+ }
109
+
84
110
  useEffect(() => {
85
111
  if (isEditing) inputRef.current.focus();
86
112
  }, [isEditing, isFocused]);
@@ -94,10 +120,8 @@ const Input = ({
94
120
  onFocus: handle.focus,
95
121
  onBlur: handle.blur,
96
122
  onKeyUp: handle.keyUp,
97
- min,
98
- max,
99
123
  ...(maskChar ? { maskChar } : {}),
100
- ...(formatChars ? { formatChars } : {}),
124
+ ...(formatChars ? { formatChars } : {})
101
125
  };
102
126
 
103
127
  function renderInput() {
@@ -106,32 +130,25 @@ const Input = ({
106
130
  }
107
131
 
108
132
  return (
109
- <>
110
- <input {...uniProps} ref={inputRef} type={isNumeric ? 'number' : type} />
111
- {isNumeric && (
112
- <div className="input__nums">
113
- <button onClick={() => handle.change(value - numStep)} className={cn(`input__num-btn`, { disabled: value <= min })}>
114
- <Minus />
115
- </button>
116
- <button onClick={() => handle.change(+value + +numStep)} className={cn(`input__num-btn`, { disabled: value >= max })}>
117
- <Plus />
118
- </button>
119
- </div>
120
- )}
121
- </>
133
+ <input {...uniProps} ref={inputRef} type={isPriceInput ? 'text' : type} />
122
134
  );
123
135
  }
124
-
136
+
125
137
  useEffect(() => {
126
138
  if (!isNotBlinkErrors && isAttemptToChange) {
127
139
  setIsAttemptToChange(null);
128
140
  setIsToHighlightError(true);
129
141
  setTimeout(() => {
130
- setIsToHighlightError(false)
142
+ setIsToHighlightError(false);
131
143
  }, blinkTime || DEFAULT_BLINK_TIME);
132
144
  }
133
145
  }, [isAttemptToChange]);
134
146
 
147
+ useEffect(() => {
148
+ if (inputRef?.current && typeof isFocusDefault === 'boolean')
149
+ setIsFocused(isFocusDefault);
150
+ }, [inputRef, isFocusDefault]);
151
+
135
152
  return (
136
153
  <div
137
154
  className={cn(
@@ -143,7 +160,12 @@ const Input = ({
143
160
  >
144
161
  {renderInput()}
145
162
  {icon}
146
- {withDelete && !isNumeric && <span className={cn(`input__close`, { hidden: !value })} onClick={handle.toggleEdit} />}
163
+ {withDelete && (
164
+ <span
165
+ className={cn(`input__close`, { hidden: !value })}
166
+ onClick={handle.toggleEdit}
167
+ />
168
+ )}
147
169
  </div>
148
170
  );
149
171
  };
@@ -1,5 +1,5 @@
1
1
  import React, { useState } from 'react';
2
- import {User} from 'react-feather';
2
+ import { User } from 'react-feather';
3
3
  import Input from './Input';
4
4
 
5
5
  global.lng = 'en';
@@ -9,48 +9,68 @@ export default {
9
9
  component: Input,
10
10
  argTypes: {
11
11
  disabled: {
12
- description: 'boolean',
12
+ description: 'boolean'
13
13
  },
14
14
  error: {
15
15
  description: 'text - coloring input if is errored'
16
16
  },
17
+ isPriceInput: {
18
+ description: 'boolean - if true, the input will be styled as PriceInput'
19
+ },
17
20
  withDelete: {
18
21
  description: 'boolean - add clear-cross by hover'
19
22
  },
20
- isNumeric: {
21
- description: 'boolean - add plus/minus buttons, force input to numeric type'
23
+ isOnlyNumber: {
24
+ description: 'boolean - only numbers'
22
25
  },
23
- numStep: {
24
- description: 'number/text - plus/minus buttons factor (default: 1)'
26
+ isOnlyString: {
27
+ description: 'boolean - only strings'
25
28
  },
26
- min: {
27
- description: 'number/text - minimal number for numeric input'
29
+ isTwoDigitAfterDot: {
30
+ description: 'boolean - only two digits after dot'
28
31
  },
29
- max: {
30
- description: 'number/text - maximal number for numeric input'
32
+ isFocusDefault: {
33
+ description: 'boolean - if true, input will be focused on mount'
31
34
  },
32
35
  placeholder: {
33
36
  description: 'text'
34
37
  },
35
38
  type: {
36
- description: "'text', 'number', 'password', 'color', 'date', 'datetime-local', 'month', 'time', 'email', 'range'",
39
+ description:
40
+ "'text', 'number', 'password', 'color', 'date', 'datetime-local', 'month', 'time', 'email', 'range'",
37
41
  control: {
38
42
  type: 'select',
39
- options: ['text', 'number', 'password', 'color', 'date', 'datetime-local', 'month', 'time', 'email', 'range']
43
+ options: [
44
+ 'text',
45
+ 'number',
46
+ 'password',
47
+ 'color',
48
+ 'date',
49
+ 'datetime-local',
50
+ 'month',
51
+ 'time',
52
+ 'email',
53
+ 'range'
54
+ ]
40
55
  }
41
56
  },
42
57
  icon: { description: 'JSX' },
43
58
  value: { description: '(* - required prop)' },
44
59
  className: { description: 'string' },
45
- mask: { description: 'string: force input to masked https://www.npmjs.com/package/react-input-mask' },
46
- symbolsLimit: { description: 'Set limit of symbols in input, overhead will be ignored' },
60
+ mask: {
61
+ description:
62
+ 'string: force input to masked https://www.npmjs.com/package/react-input-mask'
63
+ },
64
+ symbolsLimit: {
65
+ description: 'Set limit of symbols in input, overhead will be ignored'
66
+ },
47
67
  onBlur: { description: 'custom callback on blur' },
48
68
  onFocus: { description: 'custom callback on focus' },
49
69
  onKeyUp: { description: 'custom callback on keyup, returns event keyCode' }
50
70
  }
51
71
  };
52
72
 
53
- const Template = args => {
73
+ const Template = (args) => {
54
74
  const [value, setValue] = useState('');
55
75
  return <Input {...args} value={value} onChange={setValue} />;
56
76
  };
@@ -58,22 +78,17 @@ const Template = args => {
58
78
  export const InputTemplate = Template.bind({});
59
79
 
60
80
  InputTemplate.args = {
61
- type: 'number',
81
+ type: 'text',
82
+ isFocusDefault: false,
83
+ isOnlyNumber: false,
84
+ isOnlyString: false,
85
+ isPriceInput: false,
86
+ isTwoDigitAfterDot: false,
62
87
  disabled: false,
63
88
  error: '',
89
+ mask: '',
64
90
  withDelete: true,
65
- isNumeric: false,
66
- numStep: 1,
67
- // min: '0',
68
- // max: '5',
91
+ symbolsLimit: 255,
69
92
  placeholder: 'Placeholder',
70
- // mask: 'nnnnn0129',
71
- // maskChar: '_',
72
- // formatChars: {
73
- // 'n': '[0-9]',
74
- // 's': '[A-Za-z]',
75
- // '*': '[A-Za-z0-9]'
76
- // },
77
- icon: <User />,
78
- symbolsLimit: 225
93
+ icon: <User />
79
94
  };