diff --git a/libs/react/ui-core/src/index.ts b/libs/react/ui-core/src/index.ts index e734e9c..bf24742 100644 --- a/libs/react/ui-core/src/index.ts +++ b/libs/react/ui-core/src/index.ts @@ -1,3 +1,4 @@ +export * from './lib/progress/progress'; export * from './lib/switch/switch'; export * from './lib/modal/modal'; export * from './lib/no-data/no-data'; diff --git a/libs/react/ui-core/src/lib/progress/ProgressProps.d.ts b/libs/react/ui-core/src/lib/progress/ProgressProps.d.ts new file mode 100644 index 0000000..e89ec1a --- /dev/null +++ b/libs/react/ui-core/src/lib/progress/ProgressProps.d.ts @@ -0,0 +1,18 @@ +import { DefaultParams } from '../../default-types/defaultParams'; + +export interface ProgressProps extends DefaultParams { + /** Current step of the progress */ + currentStep: number; + /** Total amount of steps */ + steps: number; + /** Type of the progress */ + type?: 'linear' | 'radial'; + /** Size of the progress */ + size?: 'small' | 'standart'; + /** State of the process shown by progress */ + state?: 'progress' | 'error' | 'success'; + /** Minimum width for label in linear progress */ + minLabelWidth?: string; + /** Formatter to show progress. By default x/y */ + format?: (currentStep: number, steps: number) => string; +} diff --git a/libs/react/ui-core/src/lib/progress/progress.module.scss b/libs/react/ui-core/src/lib/progress/progress.module.scss new file mode 100644 index 0000000..201d515 --- /dev/null +++ b/libs/react/ui-core/src/lib/progress/progress.module.scss @@ -0,0 +1 @@ +@use '../../../../../../styles/components/progress' as *; diff --git a/libs/react/ui-core/src/lib/progress/progress.spec.tsx b/libs/react/ui-core/src/lib/progress/progress.spec.tsx new file mode 100644 index 0000000..d1849d7 --- /dev/null +++ b/libs/react/ui-core/src/lib/progress/progress.spec.tsx @@ -0,0 +1,10 @@ +import { render } from '@testing-library/react'; + +import { Progress } from './progress'; + +describe('Progress', () => { + it('should render successfully', () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); +}); diff --git a/libs/react/ui-core/src/lib/progress/progress.stories.tsx b/libs/react/ui-core/src/lib/progress/progress.stories.tsx new file mode 100644 index 0000000..d839e0e --- /dev/null +++ b/libs/react/ui-core/src/lib/progress/progress.stories.tsx @@ -0,0 +1,14 @@ +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { Progress } from './progress'; + +export default { + component: Progress, + title: 'Progress', +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const Primary = Template.bind({}); +Primary.args = {currentStep: 0, steps: 10}; diff --git a/libs/react/ui-core/src/lib/progress/progress.tsx b/libs/react/ui-core/src/lib/progress/progress.tsx new file mode 100644 index 0000000..b282785 --- /dev/null +++ b/libs/react/ui-core/src/lib/progress/progress.tsx @@ -0,0 +1,127 @@ +import styles from './progress.module.scss'; +import { forwardRef, memo, useMemo } from 'react'; +import { ProgressProps } from './ProgressProps'; +import { useProgress } from './useProgress'; +import Icon from '../icon/icon'; + +const LinearBar = memo(({ fillPercentage }: { fillPercentage: string }) => { + return ( +
+
+
+
+ ); +}); + +const RadialBar = memo( + ({ dashArray, dashOffset }: { dashArray: number; dashOffset: number }) => { + return ( + + + + + ); + } +); + +const Label = memo( + ({ + labelText, + state, + type, + minWidth, + }: { + labelText?: string; + state?: 'progress' | 'error' | 'success'; + type?: 'linear' | 'radial'; + minWidth?: string; + }) => { + const content = useMemo(() => { + switch (state) { + case 'progress': + return ( +
+ {labelText} +
+ ); + break; + case 'error': + return ( + + ); + break; + case 'success': + return ( + + ); + break; + default: + return
{labelText}
; + break; + } + }, [labelText, state, type]); + return content; + } +); + +export const Progress = forwardRef((props: ProgressProps, ref: any) => { + const { classes, fillPercentage, labelText, dashArray, dashOffset } = + useProgress(props); + return ( +
+ {props.type === 'linear' ? ( + + ) : ( + + )} +
+ ); +}); + +Progress.defaultProps = { + size: 'standart', + type: 'linear', + state: 'progress', + minLabelWidth: '40px', + format: (currentStep: number, steps: number) => `${currentStep}/${steps}`, +}; diff --git a/libs/react/ui-core/src/lib/progress/useProgress.ts b/libs/react/ui-core/src/lib/progress/useProgress.ts new file mode 100644 index 0000000..8d7e065 --- /dev/null +++ b/libs/react/ui-core/src/lib/progress/useProgress.ts @@ -0,0 +1,52 @@ +import styles from './progress.module.scss'; +import { useMemo } from 'react'; +import { getClasses } from '../../utils/getClasses'; +import { ProgressProps } from './ProgressProps'; + +export function useProgress(props: ProgressProps) { + const classes = useMemo(() => { + const conditions = { + 'progress': true, + 'progress-success': props.state === 'success', + 'progress-error': props.state === 'error', + 'progress-regular': props.state === 'progress', + 'progress-radial': props.type === 'radial', + 'progress-linear': props.type === 'linear', + 'progress-small': props.size === 'small', + 'progress-standart': props.size === 'standart', + }; + return getClasses(conditions, styles, props.className); + }, [props.state, props.size, props.className, props.type]); + + const fillPortion = useMemo(() => { + // Rounds to 2 digits. + const portion = Math.round((props.currentStep / props.steps) * 100) / 100; + return portion <= 1 ? (portion >= 0 ? portion : 0) : 1; + }, [props.currentStep, props.steps]); + + const dashArray = useMemo(() => { + const radius = 72; + return 2 * Math.PI * radius; + }, [props.size]); + + const dashOffset = useMemo(() => { + return dashArray * (1 - fillPortion); + }, [dashArray, fillPortion]); + + const fillPercentage = useMemo(() => { + return `${fillPortion * 100}%`; + }, [fillPortion]); + + const labelText = useMemo(() => { + return props.format?.(props.currentStep, props.steps); + }, [props.currentStep, props.steps, props.format]); + + return { + classes, + fillPortion, + fillPercentage, + labelText, + dashArray, + dashOffset, + }; +} diff --git a/styles/components/progress/index.scss b/styles/components/progress/index.scss new file mode 100644 index 0000000..ffaaed6 --- /dev/null +++ b/styles/components/progress/index.scss @@ -0,0 +1,174 @@ +@use '../../../styles/design-tokens' as dt; + +.progress { + position: relative; + box-sizing: border-box; + + .label { + font-family: dt.$typo-font-p-regular-family; + font-style: dt.$typo-font-p-regular-style; + color: dt.$general-100; + } + + .indicator { + transition: width 250ms, stroke-dashoffset 250ms; + } + + &-linear { + display: flex; + align-items: center; + gap: 15px; + width: 100%; + + .figure { + position: relative; + width: 100%; + } + + .track { + background-color: dt.$general-40; + border-radius: 10px; + } + + .indicator { + top: 0; + position: absolute; + background-color: dt.$general-100; + border-radius: 10px; + } + + .track, + .indicator { + height: 100%; + } + + .label { + font-weight: dt.$typo-font-p-regular-weight; + line-height: 24px; + text-align: end; + } + } + + &-radial { + display: inline-block; + .label, + .label-icon { + position: absolute; + transform: translate(-50%, -50%); + top: 50%; + left: 50%; + } + + .label { + font-weight: dt.$typo-font-p-medium-weight; + } + + .figure { + transform: rotate(-90deg); + } + + .track { + stroke: dt.$general-40; + } + } + + // Colors + &-success { + .label-icon { + color: dt.$green-80; + } + } + + &-error { + .label-icon { + color: dt.$red-100; + } + } + + &-linear.progress-success { + .indicator { + background-color: dt.$green-80; + } + } + &-linear.progress-error { + .indicator { + background-color: dt.$red-100; + } + } + &-linear.progress-regular { + .indicator { + background-color: dt.$primary-100; + } + } + + &-radial.progress-success { + .indicator { + stroke: dt.$green-80; + } + } + &-radial.progress-error { + .indicator { + stroke: dt.$red-100; + } + } + &-radial.progress-regular { + .indicator { + stroke: dt.$primary-100; + } + } + + // Sizes + &-linear.progress-standart { + height: 20px; + .figure { + height: 11px; + } + .label { + font-size: dt.$typo-font-p-regular-size; + } + .label-icon { + font-size: 20px; + } + } + + &-linear.progress-small { + height: 15px; + .figure { + height: 7px; + } + .label { + font-size: 12px; + } + .label-icon { + font-size: 15px; + } + } + + &-radial.progress-standart { + height: 164px; + .figure { + height: 164px; + } + .label { + font-size: 36px; + line-height: 44px; + } + .label-icon { + font-size: 55px; + } + } + + &-radial.progress-small { + height: 82px; + .figure { + height: 82px; + } + .label { + font-size: 18px; + line-height: 22px; + } + .label-icon { + font-size: 20px; + } + } +}