From 0ccc447e40b34348ceb4da308bab81e9e206aaf6 Mon Sep 17 00:00:00 2001 From: andres Date: Sat, 30 Dec 2023 13:01:04 +0100 Subject: [PATCH] feat: header component feat: dropdown component feat: avatar component --- .storybook/preview.tsx | 8 +- package.json | 1 + pnpm-lock.yaml | 98 +++++++++++++++ src/assets/icons/logout.tsx | 20 ++- .../layout/header/header.module.scss | 18 +++ .../layout/header/header.stories.tsx | 34 +++++ src/components/layout/header/header.tsx | 37 ++++++ src/components/{ui => layout}/header/index.ts | 0 .../layout/header/user-dropdown/index.ts | 1 + .../user-dropdown/user-dropdown.module.scss | 3 + .../user-dropdown/user-dropdown.stories.tsx | 27 ++++ .../header/user-dropdown/user-dropdown.tsx | 59 +++++++++ src/components/ui/avatar/avatar.module.scss | 4 + src/components/ui/avatar/avatar.stories.tsx | 18 +++ src/components/ui/avatar/avatar.tsx | 23 ++++ src/components/ui/avatar/index.ts | 1 + src/components/ui/button/button.module.scss | 22 ++-- src/components/ui/button/button.tsx | 42 ++++++- .../ui/dropdown/dropdown.module.scss | 117 ++++++++++++++++++ .../ui/dropdown/dropdown.stories.tsx | 80 ++++++++++++ src/components/ui/dropdown/dropdown.tsx | 78 ++++++++++++ src/components/ui/dropdown/index.ts | 1 + src/components/ui/header/header.module.scss | 0 src/components/ui/header/header.stories.tsx | 17 --- src/components/ui/header/header.tsx | 11 -- src/components/ui/index.ts | 4 +- src/services/decks/decks.service.ts | 11 +- src/styles/_boilerplate.scss | 9 +- src/styles/_mixins.scss | 29 +++++ src/styles/_tokens.scss | 10 ++ src/styles/index.scss | 1 + 31 files changed, 726 insertions(+), 58 deletions(-) create mode 100644 src/components/layout/header/header.module.scss create mode 100644 src/components/layout/header/header.stories.tsx create mode 100644 src/components/layout/header/header.tsx rename src/components/{ui => layout}/header/index.ts (100%) create mode 100644 src/components/layout/header/user-dropdown/index.ts create mode 100644 src/components/layout/header/user-dropdown/user-dropdown.module.scss create mode 100644 src/components/layout/header/user-dropdown/user-dropdown.stories.tsx create mode 100644 src/components/layout/header/user-dropdown/user-dropdown.tsx create mode 100644 src/components/ui/avatar/avatar.module.scss create mode 100644 src/components/ui/avatar/avatar.stories.tsx create mode 100644 src/components/ui/avatar/avatar.tsx create mode 100644 src/components/ui/avatar/index.ts create mode 100644 src/components/ui/dropdown/dropdown.module.scss create mode 100644 src/components/ui/dropdown/dropdown.stories.tsx create mode 100644 src/components/ui/dropdown/dropdown.tsx create mode 100644 src/components/ui/dropdown/index.ts delete mode 100644 src/components/ui/header/header.module.scss delete mode 100644 src/components/ui/header/header.stories.tsx delete mode 100644 src/components/ui/header/header.tsx create mode 100644 src/styles/_mixins.scss diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 5cdc25a..9d4cea3 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,7 +1,7 @@ import type { Preview } from '@storybook/react' import '@fontsource/roboto/400.css' import '../src/styles/index.scss' -import { withRouter } from 'storybook-addon-react-router-v6' +import { reactRouterParameters, withRouter } from 'storybook-addon-react-router-v6' import { themes } from '@storybook/theming' import { ToastContainer } from 'react-toastify' import 'react-toastify/dist/ReactToastify.min.css' @@ -9,6 +9,12 @@ import 'react-toastify/dist/ReactToastify.min.css' const preview: Preview = { parameters: { actions: { argTypesRegex: '^on[A-Z].*' }, + reactRouter: reactRouterParameters({ + routing: { + handle: 'Nav', + path: '*', + }, + }), controls: { matchers: { color: /(background|color)$/i, diff --git a/package.json b/package.json index 0dd1eb7..bc52933 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@hookform/resolvers": "^3.3.3", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-slider": "^1.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec2e768..d52729a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@radix-ui/react-dialog': specifier: ^1.0.5 version: 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dropdown-menu': + specifier: ^2.0.6 + version: 2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-label': specifier: ^2.0.2 version: 2.0.2(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) @@ -2540,6 +2543,33 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.6 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@types/react': 18.2.46 + '@types/react-dom': 18.2.18 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.46)(react@18.2.0): resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} peerDependencies: @@ -2633,6 +2663,44 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-menu@2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.6 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.46)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@types/react': 18.2.46 + '@types/react-dom': 18.2.18 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.46)(react@18.2.0) + dev: false + /@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==} peerDependencies: @@ -2662,6 +2730,36 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + /@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.6 + '@floating-ui/react-dom': 2.0.4(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.46)(react@18.2.0) + '@radix-ui/rect': 1.0.1 + '@types/react': 18.2.46 + '@types/react-dom': 18.2.18 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-portal@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==} peerDependencies: diff --git a/src/assets/icons/logout.tsx b/src/assets/icons/logout.tsx index f66b3ce..421daae 100644 --- a/src/assets/icons/logout.tsx +++ b/src/assets/icons/logout.tsx @@ -2,19 +2,31 @@ import { Ref, SVGProps, forwardRef, memo } from 'react' const SvgComponent = (props: SVGProps, ref: Ref) => ( - + + + + + + + ) const ForwardRef = forwardRef(SvgComponent) diff --git a/src/components/layout/header/header.module.scss b/src/components/layout/header/header.module.scss new file mode 100644 index 0000000..b513ce5 --- /dev/null +++ b/src/components/layout/header/header.module.scss @@ -0,0 +1,18 @@ +.root { + width: 100%; + height: var(--header-height); + padding: 12px var(--horizontal-padding); + + background: var(--color-dark-700); + border-bottom: 1px solid var(--color-dark-500); +} + +.content { + display: flex; + align-items: center; + justify-content: space-between; + + width: 100%; + max-width: var(--max-width); + margin: 0 auto; +} diff --git a/src/components/layout/header/header.stories.tsx b/src/components/layout/header/header.stories.tsx new file mode 100644 index 0000000..d12527a --- /dev/null +++ b/src/components/layout/header/header.stories.tsx @@ -0,0 +1,34 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { Header } from './' + +const meta = { + argTypes: { + onLogout: { action: 'logout' }, + }, + component: Header, + parameters: { + layout: 'fullscreen', + }, + tags: ['autodocs'], + title: 'Components/Header', +} satisfies Meta + +export default meta +type Story = StoryObj + +export const LoggedIn: Story = { + // @ts-expect-error onLogout is required but it is provided through argTypes + args: { + avatar: 'https://avatars.githubusercontent.com/u/1196870?v=4', + email: 'johndoe@gmail.com', + isLoggedIn: true, + userName: 'John Doe', + }, +} + +export const LoggedOut: Story = { + args: { + isLoggedIn: false, + }, +} diff --git a/src/components/layout/header/header.tsx b/src/components/layout/header/header.tsx new file mode 100644 index 0000000..130d7da --- /dev/null +++ b/src/components/layout/header/header.tsx @@ -0,0 +1,37 @@ +import { memo } from 'react' +import { Link } from 'react-router-dom' + +import { Logo } from '@/assets' +import { UserDropdown, UserDropdownProps } from '@/components/layout/header/user-dropdown' + +import s from './header.module.scss' + +import { Button } from '../../ui' + +export type HeaderProps = + | (Partial & { + isLoggedIn: false + }) + | (UserDropdownProps & { + isLoggedIn: true + }) + +export const Header = memo(({ avatar, email, isLoggedIn, onLogout, userName }: HeaderProps) => { + return ( +
+
+ + + + {isLoggedIn && ( + + )} + {!isLoggedIn && ( + + )} +
+
+ ) +}) diff --git a/src/components/ui/header/index.ts b/src/components/layout/header/index.ts similarity index 100% rename from src/components/ui/header/index.ts rename to src/components/layout/header/index.ts diff --git a/src/components/layout/header/user-dropdown/index.ts b/src/components/layout/header/user-dropdown/index.ts new file mode 100644 index 0000000..43c3e46 --- /dev/null +++ b/src/components/layout/header/user-dropdown/index.ts @@ -0,0 +1 @@ +export * from './user-dropdown' diff --git a/src/components/layout/header/user-dropdown/user-dropdown.module.scss b/src/components/layout/header/user-dropdown/user-dropdown.module.scss new file mode 100644 index 0000000..86d7484 --- /dev/null +++ b/src/components/layout/header/user-dropdown/user-dropdown.module.scss @@ -0,0 +1,3 @@ +.email { + color: var(--color-dark-100); +} diff --git a/src/components/layout/header/user-dropdown/user-dropdown.stories.tsx b/src/components/layout/header/user-dropdown/user-dropdown.stories.tsx new file mode 100644 index 0000000..ef59501 --- /dev/null +++ b/src/components/layout/header/user-dropdown/user-dropdown.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { UserDropdown } from './' + +const meta = { + argTypes: { + onLogout: { action: 'logout' }, + }, + component: UserDropdown, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + title: 'Components/UserDropdown', +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + // @ts-expect-error onLogout is required but it is provided through argTypes + args: { + avatar: 'https://avatars.githubusercontent.com/u/1196870?v=4', + email: 'johndoe@gmail.com', + userName: 'John Doe', + }, +} diff --git a/src/components/layout/header/user-dropdown/user-dropdown.tsx b/src/components/layout/header/user-dropdown/user-dropdown.tsx new file mode 100644 index 0000000..a1cb657 --- /dev/null +++ b/src/components/layout/header/user-dropdown/user-dropdown.tsx @@ -0,0 +1,59 @@ +import { ComponentPropsWithoutRef } from 'react' +import { Link } from 'react-router-dom' + +import { Logout, PersonOutline } from '@/assets' +import { + Avatar, + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, + Typography, +} from '@/components' + +import s from './user-dropdown.module.scss' + +export type UserDropdownProps = { + avatar: string + email: string + onLogout: ComponentPropsWithoutRef['onSelect'] + userName: string +} + +export const UserDropdown = ({ avatar, email, onLogout, userName }: UserDropdownProps) => { + return ( + + + + + + + +
+ {userName} + + {email} + +
+
+ + + + + My profile + + + + + + Sign out + +
+
+ ) +} diff --git a/src/components/ui/avatar/avatar.module.scss b/src/components/ui/avatar/avatar.module.scss new file mode 100644 index 0000000..f862697 --- /dev/null +++ b/src/components/ui/avatar/avatar.module.scss @@ -0,0 +1,4 @@ +.avatar { + object-fit: cover; + border-radius: 9999px; +} diff --git a/src/components/ui/avatar/avatar.stories.tsx b/src/components/ui/avatar/avatar.stories.tsx new file mode 100644 index 0000000..aa130bd --- /dev/null +++ b/src/components/ui/avatar/avatar.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { Avatar } from './' + +const meta = { + component: Avatar, + tags: ['autodocs'], + title: 'Components/Avatar', +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + src: 'https://avatars.githubusercontent.com/u/1196870?v=4', + }, +} diff --git a/src/components/ui/avatar/avatar.tsx b/src/components/ui/avatar/avatar.tsx new file mode 100644 index 0000000..7b5de17 --- /dev/null +++ b/src/components/ui/avatar/avatar.tsx @@ -0,0 +1,23 @@ +import { CSSProperties, ComponentPropsWithoutRef } from 'react' + +import clsx from 'clsx' + +import s from './avatar.module.scss' + +export type AvatarProps = ComponentPropsWithoutRef<'img'> & { + size?: CSSProperties['width'] +} + +export const Avatar = ({ className, size = '36px', style, ...rest }: AvatarProps) => { + return ( + + ) +} diff --git a/src/components/ui/avatar/index.ts b/src/components/ui/avatar/index.ts new file mode 100644 index 0000000..886c6ec --- /dev/null +++ b/src/components/ui/avatar/index.ts @@ -0,0 +1 @@ +export * from './avatar' diff --git a/src/components/ui/button/button.module.scss b/src/components/ui/button/button.module.scss index b05f111..9081aa3 100644 --- a/src/components/ui/button/button.module.scss +++ b/src/components/ui/button/button.module.scss @@ -1,4 +1,4 @@ -@mixin button { +.button { all: unset; cursor: pointer; @@ -28,7 +28,8 @@ var(--transition-duration-basic) color; &:focus-visible { - outline: 2px solid var(--color-info-700); + outline: var(--outline-focus); + outline-offset: 2px; } &:disabled { @@ -43,8 +44,6 @@ } .primary { - @include button; - color: var(--color-light-100); background-color: var(--color-accent-500); box-shadow: 0 4px 18px rgb(140 97 255 / 35%); @@ -59,8 +58,6 @@ } .secondary { - @include button; - color: var(--color-light-100); background-color: var(--color-dark-300); box-shadow: 0 2px 10px 0 #6d6d6d40; @@ -75,8 +72,6 @@ } .tertiary { - @include button; - color: var(--color-accent-500); background-color: var(--color-dark-900); border: 1px solid var(--color-accent-700); @@ -91,8 +86,6 @@ } .link { - @include button; - padding: 0.375rem 0; font-weight: var(--font-weight-bold); @@ -110,4 +103,13 @@ display: flex; align-items: center; justify-content: center; + + &:focus-visible { + outline: var(--outline-focus); + outline-offset: 2px; + } + + &.rounded { + border-radius: 9999px; + } } diff --git a/src/components/ui/button/button.tsx b/src/components/ui/button/button.tsx index b835691..3e94fb8 100644 --- a/src/components/ui/button/button.tsx +++ b/src/components/ui/button/button.tsx @@ -1,4 +1,13 @@ -import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react' +import { + ComponentPropsWithoutRef, + ElementRef, + ElementType, + ForwardedRef, + ReactNode, + forwardRef, +} from 'react' + +import { clsx } from 'clsx' import s from './button.module.scss' @@ -7,13 +16,38 @@ export type ButtonProps = { children: ReactNode className?: string fullWidth?: boolean + rounded?: boolean variant?: 'icon' | 'link' | 'primary' | 'secondary' | 'tertiary' } & ComponentPropsWithoutRef -export const Button = (props: ButtonProps) => { - const { as: Component = 'button', className, fullWidth, variant = 'primary', ...rest } = props +const ButtonPolymorph = (props: ButtonProps, ref: any) => { + const { + as: Component = 'button', + className, + fullWidth, + rounded, + variant = 'primary', + ...rest + } = props return ( - + ) } + +export const Button = forwardRef(ButtonPolymorph) as ( + props: ButtonProps & + Omit, keyof ButtonProps> & { + ref?: ForwardedRef> + } +) => ReturnType diff --git a/src/components/ui/dropdown/dropdown.module.scss b/src/components/ui/dropdown/dropdown.module.scss new file mode 100644 index 0000000..5fa5984 --- /dev/null +++ b/src/components/ui/dropdown/dropdown.module.scss @@ -0,0 +1,117 @@ +.item { + all: unset; + + cursor: pointer; + user-select: none; + + position: relative; + + display: flex; + gap: 6px; + align-items: center; + + padding: 6px 8px; + + line-height: 16px; + + border-radius: 4px; + + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; + transition-property: + color, + background-color, + border-color, + fill, + stroke, + -webkit-text-decoration-color; + + &:focus-visible { + background-color: var(--color-dark-100); + outline: none; + } + + &[data-disabled] { + pointer-events: none; + opacity: 0.5; + } +} + +.content { + position: relative; + z-index: 101; + + padding: 0 4px; + + background-color: var(--color-dark-700); + border: 1px solid var(--color-dark-500); + border-radius: 4px; + + &[data-state='open'] { + animation: fade-in 300ms linear; + } + + &[data-state='closed'] { + animation: fade-out 300ms linear; + } + + .arrow { + position: absolute; + top: -3.75px; + right: calc(50% - 3px); + transform: rotate(45deg); + + width: 9px; + height: 9px; + + background-color: var(--color-dark-700); + border: 1px solid var(--color-dark-500); + border-top: none; + border-left: none; + + stroke-width: 2px; + } +} + +.itemsBox { + > :first-child { + margin-top: 6px; + } + + > :last-child { + margin-bottom: 6px; + } +} + +.separator { + height: 1px; + margin: 6px 0.5rem; + background: var(--color-dark-300); +} + +.label { + display: flex; + gap: 8px; + align-items: center; + padding: 6px 8px; +} + +@keyframes fade-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes fade-out { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} diff --git a/src/components/ui/dropdown/dropdown.stories.tsx b/src/components/ui/dropdown/dropdown.stories.tsx new file mode 100644 index 0000000..e58aac3 --- /dev/null +++ b/src/components/ui/dropdown/dropdown.stories.tsx @@ -0,0 +1,80 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { Edit2Outline, Logout, PersonOutline, PlayCircleOutline, TrashOutline } from '@/assets' +import { + Avatar, + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components' + +const meta = { + component: DropdownMenu, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + title: 'Components/Dropdown', +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Learn: Story = { + args: {}, + render: () => ( + + + + + + + + Learn + + + + + Edit + + + + + Delete + + + + ), +} + +export const HeaderDropdown: Story = { + args: {}, + render: () => ( + + + + + + + + Learn + + + + + My profile + + + + + Sign out + + + + ), +} diff --git a/src/components/ui/dropdown/dropdown.tsx b/src/components/ui/dropdown/dropdown.tsx new file mode 100644 index 0000000..8a2bec7 --- /dev/null +++ b/src/components/ui/dropdown/dropdown.tsx @@ -0,0 +1,78 @@ +import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react' + +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +import { clsx } from 'clsx' + +import s from './dropdown.module.scss' + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuContent = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ children, className, sideOffset = 8, ...props }, ref) => ( + + +
+ +
+ +
{children}
+
+ + +)) + +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuSeparator = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuLabel = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +export { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuSeparator, + DropdownMenuTrigger, +} diff --git a/src/components/ui/dropdown/index.ts b/src/components/ui/dropdown/index.ts new file mode 100644 index 0000000..8954bfd --- /dev/null +++ b/src/components/ui/dropdown/index.ts @@ -0,0 +1 @@ +export * from './dropdown' diff --git a/src/components/ui/header/header.module.scss b/src/components/ui/header/header.module.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/ui/header/header.stories.tsx b/src/components/ui/header/header.stories.tsx deleted file mode 100644 index a022f54..0000000 --- a/src/components/ui/header/header.stories.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react' - -import { Header } from './' - -const meta = { - component: Header, - parameters: { layout: 'fullscreen' }, - tags: ['autodocs'], - title: 'Components/Header', -} satisfies Meta - -export default meta -type Story = StoryObj - -export const Default: Story = { - args: {}, -} diff --git a/src/components/ui/header/header.tsx b/src/components/ui/header/header.tsx deleted file mode 100644 index ab38367..0000000 --- a/src/components/ui/header/header.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import s from './header.module.scss' - -export type HeaderProps = { - avatar?: string - name?: string - onLogout?: () => void -} - -export const Header = ({}: HeaderProps) => { - return
Header
-} diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index 7a09a1c..594e76e 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -1,4 +1,6 @@ -export * from './header' +export * from './avatar' +export * from './dropdown' +export * from '../layout/header' export * from './button' export * from './card' export * from './typography' diff --git a/src/services/decks/decks.service.ts b/src/services/decks/decks.service.ts index cc8baa8..63d0d0c 100644 --- a/src/services/decks/decks.service.ts +++ b/src/services/decks/decks.service.ts @@ -5,20 +5,13 @@ import { DecksResponse, GetDecksArgs, UpdateDeckArgs, -} from './decks.types' -import { baseApi } from '@/services' + baseApi, +} from '@/services' const decksService = baseApi.injectEndpoints({ endpoints: builder => ({ createDeck: builder.mutation({ invalidatesTags: ['Decks'], - onQueryStarted: async (_, { dispatch, getCacheEntry, getState, queryFulfilled }) => { - const data = getCacheEntry() - const state = getState() - - decksService.util.re - await queryFulfilled - }, query: body => ({ body, method: 'POST', diff --git a/src/styles/_boilerplate.scss b/src/styles/_boilerplate.scss index 63d4884..4a6c3f1 100644 --- a/src/styles/_boilerplate.scss +++ b/src/styles/_boilerplate.scss @@ -25,8 +25,15 @@ option { color: inherit; } +a, a:visited { - color: inherit; + display: flex; + color: currentcolor; + + &:focus-visible { + outline: var(--outline-focus); + outline-offset: 2px; + } } body { diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss new file mode 100644 index 0000000..2753b0c --- /dev/null +++ b/src/styles/_mixins.scss @@ -0,0 +1,29 @@ +@mixin sm-up { + @media screen and (width > 600px) { + @content; + } +} + +@mixin md-up { + @media screen and (width > 768px) { + @content; + } +} + +@mixin lg-up { + @media screen and (width > 1024px) { + @content; + } +} + +@mixin xl-up { + @media screen and (width > 1280px) { + @content; + } +} + +@mixin xxl-up { + @media screen and (width > 1536px) { + @content; + } +} diff --git a/src/styles/_tokens.scss b/src/styles/_tokens.scss index 93e75d9..d6916e2 100644 --- a/src/styles/_tokens.scss +++ b/src/styles/_tokens.scss @@ -1,3 +1,5 @@ +@use 'mixins' as *; + :root { /* outlines */ --color-outline-focus: var(--color-info-900); @@ -11,4 +13,12 @@ /* header */ --header-height: 60px; + + /* max-width */ + --horizontal-padding: 16px; + --max-width: calc(1005px + var(--horizontal-padding) * 2); + + @include md-up { + --horizontal-padding: 24px; + } } diff --git a/src/styles/index.scss b/src/styles/index.scss index ac8e54f..b3f388c 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -1,3 +1,4 @@ +@forward 'mixins'; @forward 'colors'; @forward 'typography'; @forward 'boilerplate';