From 5c869eb2159cb713797486cb2789518ac405ec79 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 20 Nov 2025 09:26:01 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B8=D1=81=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D1=83=D0=B5=D0=BC=D1=8B=D0=B9=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=20=D0=BA=D0=BD=D0=BE?= =?UTF-8?q?=D0=BF=D0=BA=D0=B8=20=D1=81=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=B2?= =?UTF-8?q?=D0=B0=D0=B5=D0=BC=D1=8B=D0=BC=D0=B8=20=D1=80=D0=B0=D0=B7=D0=BC?= =?UTF-8?q?=D0=B5=D1=80=D0=B0=D0=BC=D0=B8,=20=D0=B2=D0=B0=D1=80=D0=B8?= =?UTF-8?q?=D0=B0=D0=BD=D1=82=D0=B0=D0=BC=D0=B8=20=D0=B8=20=D1=86=D0=B2?= =?UTF-8?q?=D0=B5=D1=82=D0=BE=D0=B2=D1=8B=D0=BC=D0=B8=20=D1=81=D1=85=D0=B5?= =?UTF-8?q?=D0=BC=D0=B0=D0=BC=D0=B8.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=B4=D0=B5=D0=BA=D0=BE=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D0=BE=D1=80=20=D1=81=D1=82=D0=B8=D0=BB=D0=B5=D0=B9=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=81=D1=82=D0=BE=D1=80=D0=B8=D0=B1=D1=83=D0=BA?= =?UTF-8?q?=D0=B0,=20=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B1=D0=B8=D0=B1=D0=BB=D0=B8=D0=BE=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/storybook/StyleDecorator.tsx | 8 ++ config/storybook/main.ts | 14 +++- config/storybook/preview.ts | 4 +- package.json | 1 + pnpm-lock.yaml | 8 ++ src/app/styles/variables.scss | 1 + src/shared/assets/chevron--left.svg | 3 + src/shared/ui/Button/Button.module.scss | 78 ++++++++++++++++++ src/shared/ui/Button/Button.stories.tsx | 102 ++++++++++++++++++++++++ src/shared/ui/Button/Button.tsx | 60 ++++++++++++++ src/shared/ui/Button/index.ts | 2 + src/shared/ui/index.ts | 2 + 12 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 config/storybook/StyleDecorator.tsx create mode 100644 src/shared/assets/chevron--left.svg create mode 100644 src/shared/ui/Button/Button.module.scss create mode 100644 src/shared/ui/Button/Button.stories.tsx create mode 100644 src/shared/ui/Button/Button.tsx create mode 100644 src/shared/ui/Button/index.ts create mode 100644 src/shared/ui/index.ts diff --git a/config/storybook/StyleDecorator.tsx b/config/storybook/StyleDecorator.tsx new file mode 100644 index 0000000..8649fc8 --- /dev/null +++ b/config/storybook/StyleDecorator.tsx @@ -0,0 +1,8 @@ +import type { Decorator } from '@storybook/react' +import '@/app/styles/index.scss' + +export const StyleDecorator: Decorator = (Story) => ( + <> + + +) diff --git a/config/storybook/main.ts b/config/storybook/main.ts index 83bffe7..0143b29 100644 --- a/config/storybook/main.ts +++ b/config/storybook/main.ts @@ -49,7 +49,13 @@ const config: StorybookConfig = { if (typeof rule === 'object' && rule !== null && 'test' in rule) { const test = rule.test if (test instanceof RegExp) { - return !(test.test('.css') || test.test('.scss') || test.test('.sass')) + // Удаляем правила для CSS/SCSS и SVG + return !( + test.test('.css') || + test.test('.scss') || + test.test('.sass') || + test.test('.svg') + ) } } return true @@ -58,6 +64,12 @@ const config: StorybookConfig = { // Использование конфигурации CSS loader из проекта config.module.rules.push(buildCssLoader(true)) + // Добавление поддержки SVGR для SVG иконок + config.module.rules.push({ + test: /\.svg$/, + use: ['@svgr/webpack'], + }) + return config }, diff --git a/config/storybook/preview.ts b/config/storybook/preview.ts index f2fe9cd..90dc982 100644 --- a/config/storybook/preview.ts +++ b/config/storybook/preview.ts @@ -1,6 +1,8 @@ import type { Preview } from '@storybook/react' +import { StyleDecorator } from './StyleDecorator.tsx' const preview: Preview = { + decorators: [StyleDecorator], parameters: { controls: { matchers: { @@ -13,7 +15,7 @@ const preview: Preview = { values: [ { name: 'light', - value: '#ffffff', + value: '#F4F5F9', }, { name: 'dark', diff --git a/package.json b/package.json index b9ddcc5..e6522ae 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "author": "", "license": "ISC", "dependencies": { + "classnames": "^2.5.1", "gsap": "^3.13.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7de266f..e708ee5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + classnames: + specifier: ^2.5.1 + version: 2.5.1 gsap: specifier: ^3.13.0 version: 3.13.0 @@ -2211,6 +2214,9 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + clean-css@5.3.3: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} engines: {node: '>= 10.0'} @@ -7278,6 +7284,8 @@ snapshots: cjs-module-lexer@1.4.3: {} + classnames@2.5.1: {} + clean-css@5.3.3: dependencies: source-map: 0.6.1 diff --git a/src/app/styles/variables.scss b/src/app/styles/variables.scss index eaf7af7..c0e24c7 100644 --- a/src/app/styles/variables.scss +++ b/src/app/styles/variables.scss @@ -6,6 +6,7 @@ --color-bg: #F4F5F9; --color-border: rgb(66 86 122 / 10%); --color-blue: #3877EE; + --color-white: #FFFFFF; // Градиенты --gradient-primary: linear-gradient(to right, #3877EE, #EF5DA8); diff --git a/src/shared/assets/chevron--left.svg b/src/shared/assets/chevron--left.svg new file mode 100644 index 0000000..ae01d1a --- /dev/null +++ b/src/shared/assets/chevron--left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/ui/Button/Button.module.scss b/src/shared/ui/Button/Button.module.scss new file mode 100644 index 0000000..1157614 --- /dev/null +++ b/src/shared/ui/Button/Button.module.scss @@ -0,0 +1,78 @@ +.button { + display: inline-flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + cursor: pointer; + transition: all 0.3s ease; + padding: 0; + outline: none; + font-family: var(--font-family-main); + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + // Variants + &.round { + border-radius: 50%; + aspect-ratio: 1; + } + + &.regular { + border-radius: 1em; + padding: 0.5em 1em; + } + + // Sizes + &.small { + height: 40px; + font-size: 14px; + } + + &.medium { + height: 50px; + font-size: 18px; + } + + &.large { + height: 60px; + font-size: 24px; + } + + // Color Schemes + &.primary { + $color-primary: var(--color-primary); + background-color: transparent; + color: $color-primary; + border: 1px solid $color-primary; + + &:hover:not(:disabled) { + background-color: var(--color-white); + } + } + + &.secondary { + $color-blue: var(--color-blue); + background-color: var(--color-white); + color: $color-blue; + box-shadow: 0px 0px 15px rgb($color-blue / 10%); + } + + // Icon handling + .icon { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + + svg { + width: 40%; + height: 40%; + object-fit: contain; + } + } +} \ No newline at end of file diff --git a/src/shared/ui/Button/Button.stories.tsx b/src/shared/ui/Button/Button.stories.tsx new file mode 100644 index 0000000..78ea6ff --- /dev/null +++ b/src/shared/ui/Button/Button.stories.tsx @@ -0,0 +1,102 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Button } from './Button' +import ChevronLeftIcon from '@/shared/assets/chevron--left.svg' + +const meta = { + title: 'Shared/Button', + component: Button, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + variant: { + control: 'select', + options: ['round', 'regular'], + description: 'Вариант внешнего вида', + }, + size: { + control: 'select', + options: ['small', 'medium', 'large'], + description: 'Размер кнопки', + }, + colorScheme: { + control: 'select', + options: ['primary', 'secondary'], + description: 'Цветовая схема', + }, + disabled: { + control: 'boolean', + description: 'Активность кнопки', + }, + onClick: { action: 'clicked' }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +/** + * Базовая кнопка + */ +export const Default: Story = { + args: { + children: 'Submit', + variant: 'regular', + size: 'medium', + colorScheme: 'primary', + }, +} + +/** + * Альтернативная цветовая схема + */ +export const SecondaryColorScheme: Story = { + args: { + children: 'Submit', + variant: 'regular', + size: 'medium', + colorScheme: 'secondary', + }, +} + +/** + * Маленькая кнопка + */ +export const Small: Story = { + args: { + children: 'Submit', + size: 'small', + }, +} + +/** + * Большая кнопка + */ +export const Large: Story = { + args: { + children: 'Submit', + size: 'large', + }, +} + +/** + * Кнопка с SVG иконкой (шеврон) + */ +export const WithIcon: Story = { + args: { + children: , + variant: 'round', + size: 'medium', + }, +} + +/** + * Отключенная кнопка + */ +export const Disabled: Story = { + args: { + children: 'Submit', + disabled: true, + }, +} diff --git a/src/shared/ui/Button/Button.tsx b/src/shared/ui/Button/Button.tsx new file mode 100644 index 0000000..8eb1195 --- /dev/null +++ b/src/shared/ui/Button/Button.tsx @@ -0,0 +1,60 @@ +import { ButtonHTMLAttributes, memo, PropsWithChildren } from 'react' +import classNames from 'classnames' +import styles from './Button.module.scss' + +export type ButtonVariant = 'round' | 'regular' +export type ButtonSize = 'small' | 'medium' | 'large' +export type ButtonColorScheme = 'primary' | 'secondary' + +export interface ButtonProps extends ButtonHTMLAttributes, PropsWithChildren { + /** + * Вариант внешнего вида кнопки + * @default 'round' + */ + variant?: ButtonVariant + /** + * Размер кнопки + * @default 'medium' + */ + size?: ButtonSize + /** + * Цветовая схема + * @default 'timeframe' + */ + colorScheme?: ButtonColorScheme +} + +/** + * Универсальный компонент кнопки для использования в слайдерах и других элементах интерфейса. + * Поддерживает различные варианты отображения, размеры и цветовые схемы. + */ +export const Button = memo((props: ButtonProps) => { + const { + className, + children, + variant = 'round', + size = 'medium', + colorScheme = 'primary', + disabled, + ...otherProps + } = props + + const mods: Record = { + [styles[variant]]: true, + [styles[size]]: true, + [styles[colorScheme]]: true, + } + + return ( + + ) +}) + +Button.displayName = 'Button' diff --git a/src/shared/ui/Button/index.ts b/src/shared/ui/Button/index.ts new file mode 100644 index 0000000..e69c4f3 --- /dev/null +++ b/src/shared/ui/Button/index.ts @@ -0,0 +1,2 @@ +export { Button } from './Button' +export type { ButtonProps, ButtonVariant, ButtonSize, ButtonColorScheme } from './Button' diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts new file mode 100644 index 0000000..d7f8c69 --- /dev/null +++ b/src/shared/ui/index.ts @@ -0,0 +1,2 @@ +export { Button } from './Button' +export type { ButtonProps } from './Button'