diff --git a/src/features/LangSwitcher/ui/LangSwitcher/LangSwitcher.tsx b/src/features/LangSwitcher/ui/LangSwitcher/LangSwitcher.tsx index 0f2a6f5..530a679 100644 --- a/src/features/LangSwitcher/ui/LangSwitcher/LangSwitcher.tsx +++ b/src/features/LangSwitcher/ui/LangSwitcher/LangSwitcher.tsx @@ -1,7 +1,12 @@ import { useTranslation } from 'react-i18next'; import React, { memo } from 'react'; -import { Button, ButtonTheme } from '@/shared/ui/deprecated/Button'; +import { + Button as ButtonDeprecated, + ButtonTheme, +} from '@/shared/ui/deprecated/Button'; import { classNames } from '@/shared/lib/classNames/classNames'; +import { ToggleFeatures } from '@/shared/lib/features'; +import { Button } from '@/shared/ui/redesigned/Button'; interface LangSwitcherProps { className?: string; @@ -16,12 +21,26 @@ export const LangSwitcher = memo(({ className, short }: LangSwitcherProps) => { }; return ( - + + {t(short ? 'EN' : 'English')} + + } + off={ + + {t(short ? 'EN' : 'English')} + + } + /> ); }); diff --git a/src/features/ThemeSwitcher/ui/ThemeSwitcher.tsx b/src/features/ThemeSwitcher/ui/ThemeSwitcher.tsx index e268747..c0cbda3 100644 --- a/src/features/ThemeSwitcher/ui/ThemeSwitcher.tsx +++ b/src/features/ThemeSwitcher/ui/ThemeSwitcher.tsx @@ -1,11 +1,17 @@ import React, { useCallback } from 'react'; -import { Button, ButtonTheme } from '@/shared/ui/deprecated/Button'; +import { + Button as ButtonDeprecated, + ButtonTheme, +} from '@/shared/ui/deprecated/Button'; import { classNames } from '@/shared/lib/classNames/classNames'; -import ThemeIcon from '../../../shared/assets/icons/theme-light.svg'; +import ThemeIconDeprecated from '../../../shared/assets/icons/theme-light.svg'; +import ThemeIcon from '../../../shared/assets/icons/theme.svg'; import { useTheme } from '@/shared/lib/hook/useTheme/useTheme'; import { saveJsonSettings } from '@/entities/User'; import { useAppDispatch } from '@/shared/lib/hook/useAppDispatch/useAppDispatch'; -import { Icon } from '@/shared/ui/deprecated/Icon'; +import { Icon as IconDeprecated } from '@/shared/ui/deprecated/Icon'; +import { ToggleFeatures } from '@/shared/lib/features'; +import { Icon } from '@/shared/ui/redesigned/Icon'; interface ThemeSwitcherProps { className?: string; @@ -23,12 +29,23 @@ export const ThemeSwitcher = ({ className }: ThemeSwitcherProps) => { }, [dispatch, toggleTheme]); return ( - + } + off={ + + + + } + /> ); }; diff --git a/src/shared/layouts/MainLayout/MainLayout.module.scss b/src/shared/layouts/MainLayout/MainLayout.module.scss index eab1dbc..2c917b2 100644 --- a/src/shared/layouts/MainLayout/MainLayout.module.scss +++ b/src/shared/layouts/MainLayout/MainLayout.module.scss @@ -2,12 +2,11 @@ min-height: 100vh; display: grid; grid-template-areas: "sidebar content rightbar"; - grid-template-columns: 284px 1fr 176px; + grid-template-columns: 284px 1fr 100px; } .sidebar { grid-area: sidebar; - justify-self: center; padding: 32px; } @@ -16,6 +15,7 @@ max-width: 1200px; justify-self: center; padding: 32px; + width: 100%; } .rightbar { diff --git a/src/shared/ui/redesigned/AppLink/AppLink.module.scss b/src/shared/ui/redesigned/AppLink/AppLink.module.scss new file mode 100644 index 0000000..b4daa67 --- /dev/null +++ b/src/shared/ui/redesigned/AppLink/AppLink.module.scss @@ -0,0 +1,15 @@ +.primary { + color: var(--text-redesigned); + + &:hover { + color: var(--accent-redesigned); + } +} + +.red { + color: var(--cancel-redesigned); + + &:hover { + opacity: 0.8; + } +} diff --git a/src/shared/ui/redesigned/AppLink/AppLink.stories.tsx b/src/shared/ui/redesigned/AppLink/AppLink.stories.tsx new file mode 100644 index 0000000..1172d28 --- /dev/null +++ b/src/shared/ui/redesigned/AppLink/AppLink.stories.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { ThemeDecorator } from '@/shared/config/storybook/ThemeDecorator/ThemeDecorator'; +import { AppLink } from './AppLink'; +import { Theme } from '@/shared/const/theme'; + +export default { + title: 'shared/AppLink', + component: AppLink, + argTypes: { + backgroundColor: { control: 'color' }, + }, + args: { + to: '/', + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const Primary = Template.bind({}); +Primary.args = { + children: 'Text', + variant: 'primary', +}; + +export const Red = Template.bind({}); +Red.args = { + children: 'Text', + variant: 'red', +}; + +export const PrimaryDark = Template.bind({}); +PrimaryDark.args = { + children: 'Text', + variant: 'primary', +}; +PrimaryDark.decorators = [ThemeDecorator(Theme.DARK)]; + +export const RedDark = Template.bind({}); +RedDark.args = { + children: 'Text', + variant: 'red', +}; +RedDark.decorators = [ThemeDecorator(Theme.DARK)]; diff --git a/src/shared/ui/redesigned/AppLink/AppLink.tsx b/src/shared/ui/redesigned/AppLink/AppLink.tsx new file mode 100644 index 0000000..5f86fed --- /dev/null +++ b/src/shared/ui/redesigned/AppLink/AppLink.tsx @@ -0,0 +1,39 @@ +import { LinkProps, NavLink } from 'react-router-dom'; +import { memo, ReactNode } from 'react'; +import { classNames } from '@/shared/lib/classNames/classNames'; +import cls from './AppLink.module.scss'; + +export type AppLinkVariant = 'primary' | 'red'; + +interface AppLinkProps extends LinkProps { + className?: string; + variant?: AppLinkVariant; + children?: ReactNode; + activeClassName?: string; +} + +export const AppLink = memo((props: AppLinkProps) => { + const { + to, + className, + children, + variant = 'primary', + activeClassName = '', + ...otherProps + } = props; + + return ( + + classNames(cls.AppLink, { [activeClassName]: isActive }, [ + className, + cls[variant], + ]) + } + {...otherProps} + > + {children} + + ); +}); diff --git a/src/shared/ui/redesigned/AppLink/index.ts b/src/shared/ui/redesigned/AppLink/index.ts new file mode 100644 index 0000000..4ffed22 --- /dev/null +++ b/src/shared/ui/redesigned/AppLink/index.ts @@ -0,0 +1 @@ +export * from './AppLink'; diff --git a/src/shared/ui/deprecated/AppLogo/AppLogo.module.scss b/src/shared/ui/redesigned/AppLogo/AppLogo.module.scss similarity index 93% rename from src/shared/ui/deprecated/AppLogo/AppLogo.module.scss rename to src/shared/ui/redesigned/AppLogo/AppLogo.module.scss index 70dc6ee..cc7bbb4 100644 --- a/src/shared/ui/deprecated/AppLogo/AppLogo.module.scss +++ b/src/shared/ui/redesigned/AppLogo/AppLogo.module.scss @@ -9,6 +9,7 @@ height: 300px; border-radius: 50%; opacity: 0.6; + pointer-events: none; } .gradientSmall { @@ -18,4 +19,5 @@ background: radial-gradient(30.29% 30.29% at 50% 50%, rgb(94 211 243 / 18%) 0%, rgb(94 211 243 / 10.6%) 25.55%, rgb(94 211 243 / 7%) 47.27%, rgb(94 211 243 / 3.8%) 69.66%, rgb(94 211 243 / 0%) 100%); border-radius: 50%; opacity: 0.6; + pointer-events: none; } diff --git a/src/shared/ui/deprecated/AppLogo/AppLogo.tsx b/src/shared/ui/redesigned/AppLogo/AppLogo.tsx similarity index 63% rename from src/shared/ui/deprecated/AppLogo/AppLogo.tsx rename to src/shared/ui/redesigned/AppLogo/AppLogo.tsx index f732098..16614e8 100644 --- a/src/shared/ui/deprecated/AppLogo/AppLogo.tsx +++ b/src/shared/ui/redesigned/AppLogo/AppLogo.tsx @@ -1,18 +1,15 @@ import React, { memo } from 'react'; import cls from './AppLogo.module.scss'; -import { HStack } from '../Stack'; +import { HStack } from '../../deprecated/Stack'; import AppSvg from '@/shared/assets/icons/app-image.svg'; import { classNames } from '@/shared/lib/classNames/classNames'; interface AppLogoProps { className?: string; + size?: number; } -/** - * Устарел, используем новые компоненты из папки redesigned - * @deprecated - */ -export const AppLogo = memo(({ className }: AppLogoProps) => ( +export const AppLogo = memo(({ className, size = 50 }: AppLogoProps) => ( ( >
- + )); diff --git a/src/shared/ui/deprecated/AppLogo/index.ts b/src/shared/ui/redesigned/AppLogo/index.ts similarity index 100% rename from src/shared/ui/deprecated/AppLogo/index.ts rename to src/shared/ui/redesigned/AppLogo/index.ts diff --git a/src/shared/ui/redesigned/Button/Button.module.scss b/src/shared/ui/redesigned/Button/Button.module.scss new file mode 100644 index 0000000..925665d --- /dev/null +++ b/src/shared/ui/redesigned/Button/Button.module.scss @@ -0,0 +1,55 @@ +.Button { + cursor: pointer; + color: var(--text-redesigned); + padding: 6px 15px; + + &:hover { + color: var(--accent-redesigned); + } +} + +.clear { + padding: 0; + border: none; + background: none; + outline: none; +} + +.square { + padding: 0; +} + +.square.m { + width: var(--font-line-m); + height: var(--font-line-m); +} + +.square.l { + width: var(--font-line-l); + height: var(--font-line-l); +} + +.square.xl { + width: var(--font-line-xl); + height: var(--font-line-xl); +} + +.m { + font: var(--font-m-redesigned); +} + +.l { + font: var(--font-l-redesigned); +} + +.xl { + font: var(--font-xl-redesigned); +} + +.disabled { + opacity: 0.5; +} + +.fullWidth { + width: 100%; +} diff --git a/src/shared/ui/redesigned/Button/Button.stories.tsx b/src/shared/ui/redesigned/Button/Button.stories.tsx new file mode 100644 index 0000000..c788f13 --- /dev/null +++ b/src/shared/ui/redesigned/Button/Button.stories.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; + +import { ThemeDecorator } from '@/shared/config/storybook/ThemeDecorator/ThemeDecorator'; +import { Button } from './Button'; +import { Theme } from '@/shared/const/theme'; + +export default { + title: 'shared/Button', + component: Button, + argTypes: { + backgroundColor: { control: 'color' }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ); + expect(screen.getByText('TEST')).toBeInTheDocument(); + }); + + test('Test clear theme', () => { + render(); + expect(screen.getByText('TEST')).toHaveClass('clear'); + // screen.debug(); + }); +}); diff --git a/src/shared/ui/redesigned/Button/Button.tsx b/src/shared/ui/redesigned/Button/Button.tsx new file mode 100644 index 0000000..a362cf9 --- /dev/null +++ b/src/shared/ui/redesigned/Button/Button.tsx @@ -0,0 +1,49 @@ +import { ButtonHTMLAttributes, memo, ReactNode } from 'react'; +import { classNames, Mods } from '@/shared/lib/classNames/classNames'; +import cls from './Button.module.scss'; + +export type ButtonVariant = 'clear' | 'outline'; + +export type ButtonSize = 'm' | 'l' | 'xl'; + +interface ButtonProps extends ButtonHTMLAttributes { + className?: string; + variant?: ButtonVariant; + square?: boolean; + size?: ButtonSize; + isDisabled?: boolean; + children?: ReactNode; + fullWidth?: boolean; +} + +export const Button = memo((props: ButtonProps) => { + const { + className, + children, + variant = 'outline', + square, + isDisabled, + size = 'm', + fullWidth, + ...otherProps + } = props; + + const mods: Mods = { + [cls.square]: square, + [cls.isDisabled]: isDisabled, + [cls.fullWidth]: fullWidth, + }; + + const additional = [className, cls[variant], cls[size]]; + + return ( + + ); +}); diff --git a/src/shared/ui/redesigned/Button/index.ts b/src/shared/ui/redesigned/Button/index.ts new file mode 100644 index 0000000..8b166a8 --- /dev/null +++ b/src/shared/ui/redesigned/Button/index.ts @@ -0,0 +1 @@ +export * from './Button'; diff --git a/src/shared/ui/redesigned/Icon/Icon.module.scss b/src/shared/ui/redesigned/Icon/Icon.module.scss new file mode 100644 index 0000000..a2d57cb --- /dev/null +++ b/src/shared/ui/redesigned/Icon/Icon.module.scss @@ -0,0 +1,16 @@ +.Icon { + color: var(--icon-redesigned); +} + +.button { + margin: 0; + padding: 0; + border: none; + outline: none; + background: none; + cursor: pointer; +} + +.button:hover .Icon { + color: var(--accent-redesigned); +} diff --git a/src/shared/ui/redesigned/Icon/Icon.tsx b/src/shared/ui/redesigned/Icon/Icon.tsx new file mode 100644 index 0000000..f9657b8 --- /dev/null +++ b/src/shared/ui/redesigned/Icon/Icon.tsx @@ -0,0 +1,58 @@ +import React, { memo } from 'react'; +import { classNames } from '@/shared/lib/classNames/classNames'; +import cls from './Icon.module.scss'; + +type SvgProps = Omit, 'onClick'>; + +interface IconBaseProps extends SvgProps { + className?: string; + Svg: React.VFC; +} + +interface NonClickableIconProps extends IconBaseProps { + clickable?: false; +} + +interface ClickableIconProps extends IconBaseProps { + clickable: true; + onClick: () => void; +} + +type IconProps = NonClickableIconProps | ClickableIconProps; + +export const Icon = memo((props: IconProps) => { + const { + className, + Svg, + width = 32, + height = 32, + clickable, + ...otherProps + } = props; + + const icon = ( + + ); + + if (clickable) { + return ( + + ); + } + + return icon; +}); diff --git a/src/shared/ui/redesigned/Icon/index.ts b/src/shared/ui/redesigned/Icon/index.ts new file mode 100644 index 0000000..e263cc0 --- /dev/null +++ b/src/shared/ui/redesigned/Icon/index.ts @@ -0,0 +1 @@ +export * from './Icon'; diff --git a/src/widgets/Sidebar/model/selectors/getSidebarItems.ts b/src/widgets/Sidebar/model/selectors/getSidebarItems.ts index 70b1331..6ea828e 100644 --- a/src/widgets/Sidebar/model/selectors/getSidebarItems.ts +++ b/src/widgets/Sidebar/model/selectors/getSidebarItems.ts @@ -1,8 +1,15 @@ import { createSelector } from '@reduxjs/toolkit'; -import MainIcon from '@/shared/assets/icons/main-20-20.svg'; -import AboutIcon from '@/shared/assets/icons/about-20-20.svg'; -import ProfileIcon from '@/shared/assets/icons/profile-20-20.svg'; -import ArticleIcon from '@/shared/assets/icons/article-20-20.svg'; + +import MainIconDeprecated from '@/shared/assets/icons/main-20-20.svg'; +import AboutIconDeprecated from '@/shared/assets/icons/about-20-20.svg'; +import ProfileIconDeprecated from '@/shared/assets/icons/profile-20-20.svg'; +import ArticleIconDeprecated from '@/shared/assets/icons/article-20-20.svg'; + +import MainIcon from '@/shared/assets/icons/home.svg'; +import ArticleIcon from '@/shared/assets/icons/article.svg'; +import AboutIcon from '@/shared/assets/icons/Info.svg'; +import ProfileIcon from '@/shared/assets/icons/avatar.svg'; + import { getUserAuthData } from '../../../../entities/User'; import { SidebarItemType } from '../types/sidebar'; import { @@ -11,17 +18,26 @@ import { getRouteMain, getRouteProfile, } from '@/shared/const/router'; +import { toggleFeatures } from '@/shared/lib/features'; export const getSidebarItems = createSelector(getUserAuthData, (userData) => { const sidebarItemsList: SidebarItemType[] = [ { path: getRouteMain(), - Icon: MainIcon, + Icon: toggleFeatures({ + name: 'isAppRedesigned', + off: () => MainIconDeprecated, + on: () => MainIcon, + }), text: 'Main page', }, { path: getRouteAbout(), - Icon: AboutIcon, + Icon: toggleFeatures({ + name: 'isAppRedesigned', + off: () => AboutIconDeprecated, + on: () => AboutIcon, + }), text: 'About site', }, ]; @@ -30,17 +46,26 @@ export const getSidebarItems = createSelector(getUserAuthData, (userData) => { sidebarItemsList.push( { path: getRouteProfile(userData.id), - Icon: ProfileIcon, + Icon: toggleFeatures({ + name: 'isAppRedesigned', + off: () => ProfileIconDeprecated, + on: () => ProfileIcon, + }), text: 'Profile', authOnly: true, }, { path: getRouteArticles(), - Icon: ArticleIcon, + Icon: toggleFeatures({ + name: 'isAppRedesigned', + off: () => ArticleIconDeprecated, + on: () => ArticleIcon, + }), text: 'Articles', authOnly: true, }, ); } + return sidebarItemsList; }); diff --git a/src/widgets/Sidebar/ui/Sidebar/Sidebar.module.scss b/src/widgets/Sidebar/ui/Sidebar/Sidebar.module.scss index 2d33f7c..82604d1 100644 --- a/src/widgets/Sidebar/ui/Sidebar/Sidebar.module.scss +++ b/src/widgets/Sidebar/ui/Sidebar/Sidebar.module.scss @@ -4,30 +4,30 @@ background: var(--inverted-bg-color); position: relative; transition: width 0.3s; -} -.switchers { - position: absolute; - bottom: 20px; - display: flex; - justify-content: center; - width: 100%; -} + .switchers { + position: absolute; + bottom: 20px; + display: flex; + justify-content: center; + width: 100%; + } -.lang { - margin-left: 20px; -} + .lang { + margin-left: 20px; + } -.collapseBtn { - position: absolute; - right: -32px; - bottom: 32px; - z-index: 10; -} + .collapseBtn { + position: absolute; + right: -32px; + bottom: 32px; + z-index: 10; + } -.items { - margin-top: 20px; - margin-left: 30px; + .items { + margin-top: 20px; + margin-left: 30px; + } } .collapsed { @@ -44,14 +44,56 @@ } .SidebarRedesigned { - height: 100%; + background: var(--light-bg-redesigned); width: 220px; + height: 100%; border-radius: 32px; - background: var(--light-bg-redesigned); position: relative; - transition: width 0.3s; + + .appLogo { + padding-top: 20px; + } + + .items { + margin-top: 40px; + } + + .collapseBtn { + position: absolute; + right: 0; + top: 50%; + z-index: 10; + transform: rotate(90deg); + } + + .switchers { + position: absolute; + bottom: 40px; + display: flex; + justify-content: center; + width: 100%; + gap: 16px; + } } -.appLogo { - padding-top: 20px; +.collapsedRedesigned { + width: 50px; + + .items { + display: flex; + align-items: center; + } + + .lang { + margin-left: 0; + } + + .collapseBtn { + transform: rotate(-90deg); + } + + .switchers { + flex-direction: column; + align-items: center; + } } diff --git a/src/widgets/Sidebar/ui/Sidebar/Sidebar.tsx b/src/widgets/Sidebar/ui/Sidebar/Sidebar.tsx index 4a717b4..c588100 100644 --- a/src/widgets/Sidebar/ui/Sidebar/Sidebar.tsx +++ b/src/widgets/Sidebar/ui/Sidebar/Sidebar.tsx @@ -1,7 +1,6 @@ import { memo, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/deprecated/Button'; -import { AppLogo } from '@/shared/ui/deprecated/AppLogo'; import { VStack } from '@/shared/ui/deprecated/Stack'; import { classNames } from '@/shared/lib/classNames/classNames'; import { getSidebarItems } from '../../model/selectors/getSidebarItems'; @@ -10,6 +9,9 @@ import cls from './Sidebar.module.scss'; import { LangSwitcher } from '../../../../features/LangSwitcher'; import { ThemeSwitcher } from '@/features/ThemeSwitcher'; import { ToggleFeatures } from '@/shared/lib/features'; +import { AppLogo } from '@/shared/ui/redesigned/AppLogo'; +import { Icon } from '@/shared/ui/redesigned/Icon'; +import ArrowIcon from '@/shared/assets/icons/arrow-bottom.svg'; interface SidebarProps { className?: string; @@ -44,11 +46,28 @@ export const Sidebar = memo(({ className }: SidebarProps) => { data-testid="sidebar" className={classNames( cls.SidebarRedesigned, - { [cls.collapsed]: collapsed }, + { [cls.collapsedRedesigned]: collapsed }, [className], )} > - + + + {itemsList} + + +
+ + +
} off={ diff --git a/src/widgets/Sidebar/ui/SidebarItem/SidebarItem.module.scss b/src/widgets/Sidebar/ui/SidebarItem/SidebarItem.module.scss index b6200a8..7969ba1 100644 --- a/src/widgets/Sidebar/ui/SidebarItem/SidebarItem.module.scss +++ b/src/widgets/Sidebar/ui/SidebarItem/SidebarItem.module.scss @@ -1,7 +1,6 @@ .item { display: flex; align-items: center; - max-height: 30px; } .link { @@ -16,7 +15,38 @@ .collapsed { .link { opacity: 0; - width: 0; transition: 0.2s opacity; + width: 0; + } +} + +.itemRedesigned { + display: flex; + align-items: center; + width: 100%; + padding: 0 16px; +} + +.collapsedRedesigned { + padding: 0 8px; + + .link { + opacity: 0; + width: 0; + margin: 0; + } +} + +.active { + background: linear-gradient(89.97deg, rgb(29 89 255 / 30%) 1.15%, rgb(94 211 243 / 0%) 99.97%); + position: relative; + + &::before { + content: ""; + position: absolute; + width: 2px; + left: 0; + background: var(--accent-redesigned); + height: 100%; } } diff --git a/src/widgets/Sidebar/ui/SidebarItem/SidebarItem.tsx b/src/widgets/Sidebar/ui/SidebarItem/SidebarItem.tsx index c645c37..8a94ff6 100644 --- a/src/widgets/Sidebar/ui/SidebarItem/SidebarItem.tsx +++ b/src/widgets/Sidebar/ui/SidebarItem/SidebarItem.tsx @@ -1,14 +1,20 @@ import { useTranslation } from 'react-i18next'; import { memo } from 'react'; import { useSelector } from 'react-redux'; -import { AppLink, AppLinkTheme } from '@/shared/ui/deprecated/AppLink'; +import { + AppLink as AppLinkDeprecated, + AppLinkTheme, +} from '@/shared/ui/deprecated/AppLink'; import { classNames } from '@/shared/lib/classNames/classNames'; import { SidebarItemType } from '../../model/types/sidebar'; import { getUserAuthData } from '../../../../entities/User'; import cls from './SidebarItem.module.scss'; +import { ToggleFeatures } from '@/shared/lib/features'; +import { AppLink } from '@/shared/ui/redesigned/AppLink'; +import { Icon } from '@/shared/ui/redesigned/Icon'; interface SidebarItemProps { - item?: SidebarItemType; + item: SidebarItemType; collapsed: boolean; } @@ -16,18 +22,37 @@ export const SidebarItem = memo(({ item, collapsed }: SidebarItemProps) => { const { t } = useTranslation(); const isAuth = useSelector(getUserAuthData); - if (item?.authOnly && !isAuth) { + if (item.authOnly && !isAuth) { return null; } return ( - - {item?.Icon && } - {t(item?.text || '')} - + + + {t(item.text || '')} + + } + off={ + + + {t(item.text || '')} + + } + /> ); });