feat: ui components

This commit is contained in:
2025-06-04 14:59:10 +03:00
parent cb799f8057
commit 8085b3bbde
25 changed files with 475 additions and 213 deletions

View File

@@ -0,0 +1,34 @@
import s from './advancedPhoneInput.module.scss';
import { DetailedHTMLProps, InputHTMLAttributes } from 'react';
import { clsx } from 'clsx';
import { Button, Input } from '@shared/ui';
type AdvancedPhoneInputProps = {
containerClassName?: string;
inputClassName?: string;
buttonClassName?: string;
onClick?: () => void;
text: string;
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
export default function AdvancedPhoneInput({
containerClassName,
inputClassName,
buttonClassName,
onClick,
text,
...props
}: AdvancedPhoneInputProps) {
return (
<div className={clsx(containerClassName, s.Container)}>
<Input {...props} className={clsx(inputClassName, s.Phone)} />
<Button
className={clsx(buttonClassName, s.Button)}
onClick={onClick}
variant='orange'
>
{text}
</Button>
</div>
);
}

View File

@@ -0,0 +1,19 @@
.Container {
position: relative;
//width: 100%;
}
.Phone {
width: 100%;
position: relative;
z-index: 2;
padding-left: 2vw;
}
.Button {
position: absolute;
right: 4px;
top: calc(50% - 25px);
z-index: 3;
min-height: calc(100% - 4px);
}

View File

@@ -0,0 +1 @@
export { default as AdvancedPhoneInput } from './advanced-phone-input';

View File

@@ -2,17 +2,22 @@
display: flex;
align-items: center;
justify-content: center;
padding: 13px 33px;
padding: 10px 32px;
border-radius: 28px;
min-height: 55px;
font-family: $font-open-sans;
font-weight: $font-regular;
font-size: 24px;
line-height: 1;
line-height: 100%;
transition: all 0.15s linear;
white-space: nowrap;
width: max-content;
&_fullWidth {
width: 100%;
}
svg {
width: 18px;
@@ -24,6 +29,11 @@
&:hover {
cursor: pointer;
box-shadow: 1px 1px 1px 0px $color-darkgray;
}
&:active {
box-shadow: inset 1px 1px 2px 0px $color-darkgray;
}
&:hover svg {
@@ -40,6 +50,12 @@
color: $color-white;
}
&_ghost {
background: transparent;
color: $color-white;
border: 1px solid $color-gray-border;
}
&_disabled {
cursor: not-allowed;
}

View File

@@ -1,5 +1,11 @@
import s from './button.module.scss';
import { FunctionComponent, HTMLAttributes, ReactNode, SVGProps } from 'react';
import {
ButtonHTMLAttributes,
DetailedHTMLProps,
FunctionComponent,
ReactNode,
SVGProps,
} from 'react';
import { clsx } from 'clsx';
type ButtonProps = {
@@ -9,7 +15,11 @@ type ButtonProps = {
Icon?: FunctionComponent<SVGProps<SVGSVGElement>>;
onClick?: () => void;
variant?: 'default' | 'orange' | 'ghost';
} & HTMLAttributes<HTMLButtonElement>;
fullWidth?: boolean;
} & DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>;
export default function Button({
className,
@@ -18,6 +28,7 @@ export default function Button({
Icon,
disabled,
variant = 'default',
fullWidth = false,
...props
}: ButtonProps) {
return (
@@ -26,6 +37,7 @@ export default function Button({
s.Button,
disabled && s.Button_disabled,
s['Button_' + variant],
fullWidth && s.Button_fullWidth,
className,
)}
onClick={onClick}

View File

@@ -1,3 +1,5 @@
export { Button } from './button';
export { Mark } from './mark';
export { Input } from './input';
export { AdvancedPhoneInput } from './advanced-phone-input';
export { TextArea } from './text-area';

View File

@@ -1,113 +1,54 @@
.input {
position: relative;
.Input {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
width: 100%;
height: 42px;
background: $color-white;
border: 1px solid $color-darkgray;
border-radius: 28px;
padding: 10px 24px;
transition: border ease .5s;
//@include iftablet {
// height: rem(53px);
//}
font-family: $font-open-sans;
font-weight: $font-regular;
font-size: 24px;
line-height: 100%;
color: $color-text;
width: max-content;
&__self {
&:focus {
border-color: $color-orange;
transition: border-color ease .5s;
}
&:hover {
border-color: $color-text;
transition: border-color ease .5s;
}
&:focus:hover {
border-color: $color-orange;
transition: border-color ease .5s;
}
&_fullWidth{
width: 100%;
height: 100%;
padding: 22px 16px 6px 16px;
font-size: 14px;
line-height: 100%;
color: $color-text;
opacity: 0.9;
//@include iftablet {
// padding: rem(26px) rem(16px) rem(6px) rem(16px);
// font-size: rem(16px);
// line-height: 130%;
//}
}
&__placeholder {
position: absolute;
top: 12px;
left: 16px;
font-size: 14px;
line-height: 130%;
color: $color-darkgray;
opacity: 0.4;
transition: all 0.15s ease-in;
&_ghost {
background: transparent;
color: $color-white;
//@include iftablet {
// top: rem(16px);
// font-size: rem(16px);
//}
}
&__placeholder_active {
top: 6px;
left: 16px;
font-size: 11px;
color: $color-darkgray;
//@include iftablet {
// top: rem(6px);
// left: rem(16px);
// font-size: rem(14px);
//}
}
&--error {
border-color: $color-error !important;
}
&__errorMessage {
margin-top: 10px;
font-size: 12px;
line-height: 130%;
color: $color-error;
//@include iftablet {
// margin-top: 6px;
//}
}
&--disabled {
cursor: not-allowed;
color: $color-lightgray;
}
&__password-rules {
display: flex;
align-items: center;
margin-top: 6px;
gap: 6px;
//@include iftablet {
// gap: rem(10px);
//}
//@include ifdesktop {
// margin-top: rem(9px);
//}
&__rule-block {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
&:focus {
border-color: $color-orange;
transition: border-color ease .5s;
}
&__rule-text {
font-family: $font-open-sans;
font-size: 11px;
line-height: 100%;
color: rgba(35, 48, 56, 0.7);
&:hover {
border-color: $color-white;
transition: border-color ease .5s;
}
//@include iftablet {
// font-size: rem(14px);
//}
&:focus:hover {
border-color: $color-orange;
transition: border-color ease .5s;
}
}
}
}

View File

@@ -2,67 +2,32 @@
import s from './input.module.scss';
import React, { InputHTMLAttributes, useState } from 'react';
import clsx from 'clsx';
import { DetailedHTMLProps, InputHTMLAttributes } from 'react';
import { clsx } from 'clsx';
type InputPropsType = {
type InputProps = {
wrapperClassName?: string;
className?: string;
placeholder: string;
errorMessage?: string | boolean;
} & InputHTMLAttributes<HTMLInputElement>;
fullWidth?: boolean;
variant?: 'default' | 'ghost';
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
const Input = ({
placeholder,
errorMessage,
disabled,
className,
onChange,
fullWidth = false,
variant = 'default',
...props
}: InputPropsType) => {
const [onFocus, setOnFocus] = useState(false);
const hasValue =
typeof props.value === 'string'
? props.value.length > 0
: props.value !== undefined && props.value !== null;
}: InputProps) => {
return (
<div>
<div
className={clsx(s.input, className, {
[s['input--error']]: !!errorMessage,
})}
>
<label
className={clsx(s.input__placeholder, {
[s.input__placeholder_active]: hasValue || onFocus,
})}
>
{placeholder}
</label>
<input
{...props}
onChange={onChange}
onFocus={(event) => {
if (typeof props.onFocus !== 'undefined') {
props.onFocus(event);
}
setOnFocus(true);
}}
onBlur={(event) => {
if (typeof props.onBlur !== 'undefined') {
props.onBlur(event);
}
setOnFocus(false);
}}
disabled={disabled}
className={clsx(s.input__self, { [s['input--disabled']]: disabled })}
/>
</div>
{errorMessage && (
<span className={s.input__errorMessage}>{errorMessage}</span>
<input
{...props}
className={clsx(
s.Input,
s['Input_' + variant],
fullWidth && s.Input_fullWidth,
className,
)}
</div>
/>
);
};

View File

View File

@@ -0,0 +1 @@
export { default as TextArea } from './text-area';

View File

@@ -0,0 +1,103 @@
.Container {
display: flex;
flex-direction: column;
box-sizing: border-box;
width: max-content;
/* Allows the `resize` property to work on the div */
overflow: hidden;
&_fullWidth{
width: 100%;
}
}
.Area {
background: $color-white;
border: 1px solid $color-darkgray;
border-radius: 20px;
padding: 10px 24px;
transition: border ease .5s;
font-family: $font-open-sans;
font-weight: $font-regular;
font-size: 24px;
line-height: 100%;
color: $color-text;
resize: none;
display: block;
width: 100%;
flex-grow: 1;
/* scrollbar */
& {
/* Arrow mouse cursor over the scrollbar */
cursor: auto;
}
&::-webkit-scrollbar {
width: 12px;
height: 12px;
}
&::-webkit-scrollbar-track,
&::-webkit-scrollbar-thumb {
background-clip: content-box;
border: 4px solid transparent;
border-radius: 12px;
}
&::-webkit-scrollbar-track {
background-color: #333; // цвет дорожки
}
&::-webkit-scrollbar-thumb {
background-color: #999; // цвет указателя
}
@media (hover: hover) {
:where(&:not(:disabled))::-webkit-scrollbar-thumb:hover {
background-color: #999;
}
}
&:where(:autofill) {
/* Reliably removes native autofill colors */
background-clip: text;
-webkit-text-fill-color: #999;
}
&:focus {
border-color: $color-orange;
transition: border-color ease .5s;
}
&:hover {
border-color: $color-text;
transition: border-color ease .5s;
}
&:focus:hover {
border-color: $color-orange;
transition: border-color ease .5s;
}
&:focus-visible {
outline: none;
}
&_ghost {
background: transparent;
color: $color-white;
&:focus {
border-color: $color-orange;
transition: border-color ease .5s;
}
&:hover {
border-color: $color-white;
transition: border-color ease .5s;
}
&:focus:hover {
border-color: $color-orange;
transition: border-color ease .5s;
}
}
}

View File

@@ -0,0 +1,32 @@
import s from './text-area.module.scss';
import { DetailedHTMLProps, ReactNode, TextareaHTMLAttributes } from 'react';
import { clsx } from 'clsx';
type TextAreaProps = {
className?: string;
children?: ReactNode;
variant?: 'default' | 'ghost';
fullWidth?: boolean;
} & DetailedHTMLProps<
TextareaHTMLAttributes<HTMLTextAreaElement>,
HTMLTextAreaElement
>;
export default function TextArea({
className,
children,
variant = 'default',
fullWidth = false,
...props
}: TextAreaProps) {
return (
<div className={clsx(s.Container, fullWidth && s.Container_fullWidth)}>
<textarea
{...props}
className={clsx(className, s.Area, s['Area_' + variant])}
>
{children}
</textarea>
</div>
);
}